batchToken) {
$this->batchToken = uniqid();
return $this->batchToken;
}
}
/**
* @param $token
* @param int $count
*/
public function endBatch($token, $count = 100)
{
if ($token && $this->batchToken === $token) {
$this->batchToken = null;
$previousLoopCount = null;
while (($loopCount = $this->getQueueCount()) > 0) {
if ($previousLoopCount !== null && $previousLoopCount <= $loopCount) {
break; // avoid to be blocked in loops if messages can not be processed
}
$previousLoopCount = $loopCount;
$this->processUpdateQueue($count);
}
return true;
}
return false;
}
/**
* @param int $count
*/
public function processUpdateQueue($count = 10)
{
global $prefs;
if (! isset($prefs['unified_engine'])) {
return;
}
if ($this->batchToken) {
return;
}
$queuelib = TikiLib::lib('queue');
$toProcess = $queuelib->pull(self::INCREMENT_QUEUE, $count);
if ($this->rebuildInProgress()) {
// Requeue to add to new index too (that is rebuilding)
$queuelib->pushAll(self::INCREMENT_QUEUE_REBUILD, $toProcess);
}
$access = TikiLib::lib('access');
$access->preventRedirect(true);
if (count($toProcess)) {
$indexer = null;
try {
// Since the object being updated may have category changes during the update,
// make sure internal permission cache does not refer to the pre-update situation.
Perms::getInstance()->clear();
$index = $this->getIndex('data-write');
$index = new Search_Index_TypeAnalysisDecorator($index);
$indexer = $this->buildIndexer($index);
$indexer->update($toProcess);
if ($prefs['storedsearch_enabled'] == 'y') {
// Stored search relation adding may cause residual index backlog
$toProcess = $queuelib->pull(self::INCREMENT_QUEUE, $count);
$indexer->update($toProcess);
}
// Detect newly created identifier fields
$initial = array_flip($prefs['unified_identifier_fields']);
$collected = array_flip($index->getIdentifierFields());
$combined = array_merge($initial, $collected);
// Store preference only on change
if (count($combined) > count($initial)) {
$tikilib = TikiLib::lib('tiki');
$tikilib->set_preference('unified_identifier_fields', array_keys($combined));
}
} catch (Exception $e) {
// Re-queue pulled messages for next update
foreach ($toProcess as $message) {
$queuelib->push(self::INCREMENT_QUEUE, $message);
}
Feedback::error(
tr('The search index could not be updated. The site is misconfigured. Contact an administrator.') .
'
' . $e->getMessage()
);
}
if ($indexer) {
$indexer->clearSources();
}
}
$access->preventRedirect(false);
}
/**
* @return array
*/
public function getQueueCount()
{
$queuelib = TikiLib::lib('queue');
return $queuelib->count(self::INCREMENT_QUEUE);
}
/**
* @return bool
*/
public function rebuildInProgress()
{
global $prefs;
if ($prefs['unified_engine'] == 'elastic') {
$name = $this->getIndexLocation('data');
$connection = $this->getElasticConnection(true);
return $connection->isRebuilding($name);
} elseif ($prefs['unified_engine'] == 'mysql') {
$lockName = TikiLib::lib('tiki')->get_preference('unified_mysql_index_rebuilding');
return empty($lockName) ? false : TikiDb::get()->isLocked($lockName);
}
return false;
}
/**
* @param int $loggit 0=no logging, 1=log to Search_Indexer.log, 2=log to Search_Indexer_console.log
* @param bool $fallback If the fallback index is being rebuild
* @param Symfony\Component\Console\Helper\ProgressBar $progress progress bar object from rebuild console command
*
* @return array|bool
* @throws Exception
*/
public function rebuild($loggit = 0, $fallback = false, $progress = null)
{
global $prefs;
$engineResults = null;
$tikilib = TikiLib::lib('tiki');
switch ($prefs['unified_engine']) {
case 'elastic':
$connection = $this->getElasticConnection(true);
$aliasName = $prefs['unified_elastic_index_prefix'] . 'main';
$indexName = $aliasName . '_' . uniqid();
$index = new Search_Elastic_Index($connection, $indexName);
$engineResults = new Search_EngineResult_Elastic($index);
$index->setCamelCaseEnabled($prefs['unified_elastic_camel_case'] == 'y');
TikiLib::events()->bind(
'tiki.process.shutdown',
function () use ($indexName, $index) {
global $prefs;
if ($prefs['unified_elastic_index_current'] !== $indexName) {
$index->destroy();
}
}
);
break;
case 'mysql':
$indexName = 'index_' . uniqid();
$indexesToRestore = $this->getIndexesToRestore();
$index = new Search_MySql_Index(TikiDb::get(), $indexName);
$engineResults = new Search_EngineResult_MySQL($index);
$tikilib->set_preference('unified_mysql_index_rebuilding', $indexName);
TikiDb::get()->getLock($indexName);
TikiLib::events()->bind(
'tiki.process.shutdown',
function () use ($indexName, $index) {
global $prefs;
if ($prefs['unified_mysql_index_current'] !== $indexName) {
$index->destroy();
}
}
);
break;
default:
Feedback::error(tr('Unsupported index type "%0". Needs to be one of "mysql" or "elastic". Try resaving the Search Control Panel', $prefs['unified_engine']));
return [];
}
// Build in -new
if (! $fallback) {
TikiLib::lib('queue')->clear(self::INCREMENT_QUEUE);
TikiLib::lib('queue')->clear(self::INCREMENT_QUEUE_REBUILD);
}
$access = TikiLib::lib('access');
$access->preventRedirect(true);
$this->isRebuildingNow = true;
$stat = [];
$indexer = null;
$totalFieldsUsedIn = 'total fields used in the ' . $prefs['unified_engine'] . ' search index: ';
try {
$indexDecorator = new Search_Index_TypeAnalysisDecorator($index);
$indexer = $this->buildIndexer($indexDecorator, $loggit);
$lastStats = $tikilib->get_preference('unified_last_rebuild_stats', [], true);
$stat = $tikilib->allocate_extra(
'unified_rebuild',
function () use ($indexer, $lastStats, $progress) {
return $indexer->rebuild($lastStats, $progress);
}
);
if (! empty($indexesToRestore)) {
$index->restoreOldIndexes($indexesToRestore, $indexName);
$index->endUpdate();
}
$stat['total tiki fields indexed'] = $indexDecorator->getFieldCount();
if (! is_null($engineResults)) {
$fieldsCount = $engineResults->getEngineFieldsCount();
if ($fieldsCount !== $stat['total tiki fields indexed']) {
$stat[$totalFieldsUsedIn] = $fieldsCount;
}
$tikilib->set_preference('unified_total_fields', $fieldsCount);
}
$tikilib->set_preference('unified_field_count', $indexDecorator->getFieldCount());
$tikilib->set_preference('unified_identifier_fields', $indexDecorator->getIdentifierFields());
$stats = [];
$stats['default'] = $stat;
// Force destruction to clear locks
if ($indexer) {
$indexer->clearSources();
$indexer->log->info("Indexed");
foreach ($stats['default']['counts'] as $key => $val) {
$indexer->log->info(" $key: $val");
}
$indexer->log->info(" total tiki fields indexed: {$stats['default']['total tiki fields indexed']}");
$indexer->log->info(" total fields used in the mysql search index: : {$stats['default'][$totalFieldsUsedIn]}");
unset($indexer);
}
unset($indexDecorator, $index);
$oldIndex = null;
switch ($prefs['unified_engine']) {
case 'elastic':
$oldIndex = null; // assignAlias will handle the clean-up
$tikilib->set_preference('unified_elastic_index_current', $indexName);
$connection->assignAlias($aliasName, $indexName);
break;
case 'mysql':
// Obtain the old index and destroy it after permanently replacing it.
$oldIndex = $this->getIndex('data', false);
$tikilib->set_preference('unified_mysql_index_current', $indexName);
TikiDb::get()->releaseLock($indexName);
break;
}
if ($oldIndex) {
if (! $oldIndex->destroy()) {
Feedback::error(tr('Failed to delete the old index.'));
}
}
} catch (Exception $e) {
$stats['default']['error'] = true;
$stats['default']['error_message'] = tr('The search index could not be rebuilt.') . ' ' . $e->getMessage();
Feedback::error(tr('The search index could not be rebuilt.') . '
' . $e->getMessage());
}
if ($fallback) {
// Fallback index was rebuilt. Proceed with default index operations
return $stats['default'];
}
// Rebuild mysql as fallback for elasticsearch engine
list($fallbackEngine, $fallbackEngineName, $fallbackVersion) = TikiLib::lib('unifiedsearch')->getFallbackEngineDetails();
if (! $fallback && $fallbackEngine) {
$defaultEngine = $prefs['unified_engine'];
$prefs['unified_engine'] = $fallbackEngine;
$stats['fallback'] = $this->rebuild($loggit, true);
$prefs['unified_engine'] = $defaultEngine;
$log = new Laminas\Log\Writer\Stream($this->getLogFilename($loggit, $defaultEngine), 'a');
$loggerInstance = new Laminas\Log\Logger();
$loggerInstance->addWriter($log);
$loggerInstance->info('Fallback:');
$loggerInstance->info(" Engine $fallbackEngineName" . (empty($fallbackVersion) ? '' : ", version $fallbackVersion"));
$loggerInstance->info(" Index $indexName");
$loggerInstance->info(' Detailed information at ' . $this->getLogFilename($loggit, $fallbackEngine));
}
// Requeue messages that were added and processed in old index,
// while rebuilding the new index
$queueLib = TikiLib::lib('queue');
$toProcess = $queueLib->pull(
self::INCREMENT_QUEUE_REBUILD,
$queueLib->count(self::INCREMENT_QUEUE_REBUILD)
);
$queueLib->pushAll(self::INCREMENT_QUEUE, $toProcess);
// Process the documents updated while we were processing the update
$this->processUpdateQueue(1000);
if ($prefs['storedsearch_enabled'] == 'y') {
TikiLib::lib('storedsearch')->reloadAll();
}
$tikilib->set_preference('unified_last_rebuild', $tikilib->now);
$tikilib->set_preference('unified_last_rebuild_stats', $stats);
$this->isRebuildingNow = false;
$access->preventRedirect(false);
return $stats;
}
/**
* Return the current engine for unified search, version and current index name/table
* @return array
*/
public function getCurrentEngineDetails()
{
global $prefs;
global $tikilib;
switch ($prefs['unified_engine']) {
case 'elastic':
$elasticsearch = new \Search_Elastic_Connection($prefs['unified_elastic_url']);
$engine = 'Elastic';
$version = $elasticsearch->getVersion();
$index = $prefs['unified_elastic_index_current'];
break;
case 'mysql':
$engine = 'MySQL';
$version = $tikilib->getMySQLVersion();
$index = $prefs['unified_mysql_index_current'];
break;
default:
$engine = '';
$version = '';
$index = '';
break;
}
return [$engine, $version, $index];
}
/**
* Get the index location depending on $tikidomain for multi-tiki
*
* @param string $indexType
* @param string $engine If not set, it uses default unified search engine
* @return string path to index directory
* @throws Exception
*/
private function getIndexLocation($indexType = 'data', $engine = null)
{
global $prefs, $tikidomain;
$mapping = [
'elastic' => [
'data' => $prefs['unified_elastic_index_current'],
'preference' => $prefs['unified_elastic_index_prefix'] . 'pref_' . $prefs['language'],
'connect' => $prefs['unified_elastic_index_prefix'] . 'connect',
],
'mysql' => [
'data' => $prefs['unified_mysql_index_current'],
'preference' => 'index_' . 'pref_' . $prefs['language'],
'connect' => 'index_connect',
],
];
$engine = $engine ?: $prefs['unified_engine'];
if (isset($mapping[$engine][$indexType])) {
$index = $mapping[$engine][$indexType];
return $index;
} else {
throw new Exception('Internal: Invalid index requested: ' . $indexType);
}
}
/**
* @param $type
* @param $objectId
*/
public function invalidateObject($type, $objectId)
{
TikiLib::lib('queue')->push(
self::INCREMENT_QUEUE,
[
'object_type' => $type,
'object_id' => $objectId
]
);
}
/**
* Invalidate the indices cache
*/
public function invalidateIndicesCache()
{
$this->indices = [];
}
/**
* @return array
*/
public function getSupportedTypes()
{
global $prefs;
$types = [];
if ($prefs['feature_wiki'] == 'y') {
$types['wiki page'] = tra('wiki page');
}
if ($prefs['feature_blogs'] == 'y') {
$types['blog post'] = tra('blog post');
}
if ($prefs['feature_articles'] == 'y') {
$types['article'] = tra('article');
}
if ($prefs['feature_file_galleries'] == 'y') {
$types['file'] = tra('file');
$types['file gallery'] = tra('file gallery');
}
if ($prefs['feature_forums'] == 'y') {
$types['forum post'] = tra('forum post');
$types['forum'] = tra('forum');
}
if ($prefs['feature_trackers'] == 'y') {
$types['trackeritem'] = tra('tracker item');
$types['tracker'] = tra('tracker');
$types['trackerfield'] = tra('tracker field');
}
if ($prefs['feature_sheet'] == 'y') {
$types['sheet'] = tra('sheet');
}
if (
$prefs['feature_wiki_comments'] == 'y'
|| $prefs['feature_article_comments'] == 'y'
|| $prefs['feature_poll_comments'] == 'y'
|| $prefs['feature_file_galleries_comments'] == 'y'
|| $prefs['feature_trackers'] == 'y'
) {
$types['comment'] = tra('comment');
}
if ($prefs['feature_categories'] === 'y') {
$types['category'] = tra('category');
}
if ($prefs['feature_webservices'] === 'y') {
$types['webservice'] = tra('webservice');
}
if ($prefs['activity_basic_events'] === 'y' || $prefs['activity_custom_events'] === 'y') {
$types['activity'] = tra('activity');
}
if ($prefs['feature_calendar'] === 'y') {
$types['calendaritem'] = tra('calendar item');
$types['calendar'] = tra('calendar');
}
$types['user'] = tra('user');
$types['group'] = tra('group');
return $types;
}
/**
* Read log files
*
* @param int $num Number of items to return
* @param string $needle Search for a string
* @param bool $reverse Reverse the results to return the first or the last lines
* @return array
*/
public function getLogItems($num = -1, $needle = '', $reverse = false)
{
$files['web'] = $this->getLogFilename(1);
$files['console'] = $this->getLogFilename(2);
$resultLines = [];
foreach ($files as $type => $filename) {
$count = 1;
$handle = fopen($filename, "r");
if ($handle) {
$resultLines[$type]['logs'] = [];
$resultLines[$type]['file'] = $filename;
while (($line = fgets($handle)) !== false) {
$pos = strpos($line, $needle);
if (empty($needle) || $pos !== false) {
array_push($resultLines[$type]['logs'], $line);
}
$count++;
}
fclose($handle);
}
if (! empty($resultLines[$type]['logs'])) {
if ($reverse) {
$resultLines[$type]['logs'] = array_reverse($resultLines[$type]['logs']);
}
if ($num > -1) {
$resultLines[$type]['logs'] = array_slice($resultLines[$type]['logs'], -$num);
}
} else {
unset($resultLines[$type]);
}
}
return $resultLines;
}
public function getLastLogItem()
{
$files['web'] = $this->getLogFilename(1);
$files['console'] = $this->getLogFilename(2);
foreach ($files as $type => $file) {
if ($fp = @fopen($file, "r")) {
$pos = -2;
$t = " ";
while ($t != "\n") {
if (! fseek($fp, $pos, SEEK_END)) {
$t = fgetc($fp);
$pos = $pos - 1;
} else {
rewind($fp);
break;
}
}
$t = fgets($fp);
fclose($fp);
$ret[$type] = $t;
} else {
$ret[$type] = '';
}
}
return $ret;
}
/**
* @param $index
* @param int $loggit 0=no logging, 1=log to Search_Indexer.log, 2=log to Search_Indexer_console.log
* @return Search_Indexer
*/
private function buildIndexer($index, $loggit = 0)
{
global $prefs;
$isRepository = $index instanceof Search_Index_QueryRepository;
if (! $isRepository && method_exists($index, 'getRealIndex')) {
$isRepository = $index->getRealIndex() instanceof Search_Index_QueryRepository;
}
if (! $this->isRebuildingNow && $isRepository && $prefs['storedsearch_enabled'] == 'y') {
$index = new Search_Index_QueryAlertDecorator($index);
}
if (! empty($prefs['unified_excluded_categories'])) {
$index = new Search_Index_CategoryFilterDecorator(
$index,
array_filter(
array_map(
'intval',
$prefs['unified_excluded_categories']
)
)
);
}
$logWriter = null;
if ($loggit) {
$logWriter = new Laminas\Log\Writer\Stream($this->getLogFilename($loggit), 'w');
}
$indexer = new Search_Indexer($index, $logWriter);
$this->addSources($indexer, 'indexing');
if ($prefs['unified_tokenize_version_numbers'] == 'y') {
$indexer->addContentFilter(new Search_ContentFilter_VersionNumber());
}
return $indexer;
}
public function getDocuments($type, $object)
{
$indexer = $this->buildIndexer($this->getIndex());
return $indexer->getDocuments($type, $object);
}
public function getAvailableFields()
{
$indexer = $this->buildIndexer($this->getIndex());
return $indexer->getAvailableFields();
}
/**
* @param Search_Indexer $aggregator
* @param string $mode
*/
private function addSources($aggregator, $mode = 'indexing')
{
global $prefs;
$types = $this->getSupportedTypes();
// Content Sources
if (isset($types['trackeritem'])) {
$aggregator->addContentSource('trackeritem', new Search_ContentSource_TrackerItemSource($mode));
$aggregator->addContentSource('tracker', new Search_ContentSource_TrackerSource());
$aggregator->addContentSource('trackerfield', new Search_ContentSource_TrackerFieldSource());
}
if (isset($types['forum post'])) {
$aggregator->addContentSource('forum post', new Search_ContentSource_ForumPostSource());
$aggregator->addContentSource('forum', new Search_ContentSource_ForumSource());
}
if (isset($types['blog post'])) {
$aggregator->addContentSource('blog post', new Search_ContentSource_BlogPostSource());
}
if (isset($types['article'])) {
$articleSource = new Search_ContentSource_ArticleSource();
$aggregator->addContentSource('article', $articleSource);
$aggregator->addGlobalSource(new Search_GlobalSource_ArticleAttachmentSource($articleSource));
}
if (isset($types['file'])) {
$fileSource = new Search_ContentSource_FileSource();
$aggregator->addContentSource('file', $fileSource);
$aggregator->addContentSource('file gallery', new Search_ContentSource_FileGallerySource());
$aggregator->addGlobalSource(new Search_GlobalSource_FileAttachmentSource($fileSource));
}
if (isset($types['sheet'])) {
$aggregator->addContentSource('sheet', new Search_ContentSource_SheetSource());
}
if (isset($types['comment'])) {
$commentTypes = [];
if ($prefs['feature_wiki_comments'] == 'y') {
$commentTypes[] = 'wiki page';
}
if ($prefs['feature_article_comments'] == 'y') {
$commentTypes[] = 'article';
}
if ($prefs['feature_poll_comments'] == 'y') {
$commentTypes[] = 'poll';
}
if ($prefs['feature_file_galleries_comments'] == 'y') {
$commentTypes[] = 'file gallery';
}
if ($prefs['feature_trackers'] == 'y') {
$commentTypes[] = 'trackeritem';
}
$aggregator->addContentSource('comment', new Search_ContentSource_CommentSource($commentTypes));
$aggregator->addGlobalSource(new Search_GlobalSource_CommentSource());
}
if (isset($types['user'])) {
$aggregator->addContentSource('user', new Search_ContentSource_UserSource($prefs['user_in_search_result']));
}
if (isset($types['group'])) {
$aggregator->addContentSource('group', new Search_ContentSource_GroupSource());
}
if (isset($types['calendar'])) {
$aggregator->addContentSource('calendaritem', new Search_ContentSource_CalendarItemSource());
$aggregator->addContentSource('calendar', new Search_ContentSource_CalendarSource());
}
if ($prefs['activity_custom_events'] == 'y' || $prefs['activity_basic_events'] == 'y' || $prefs['monitor_enabled'] == 'y') {
$aggregator->addContentSource('activity', new Search_ContentSource_ActivityStreamSource($aggregator instanceof Search_Indexer ? $aggregator : null));
}
if ($prefs['goal_enabled'] == 'y') {
$aggregator->addContentSource('goalevent', new Search_ContentSource_GoalEventSource());
}
if ($prefs['feature_webservices'] === 'y') {
$aggregator->addContentSource('webservice', new Search_ContentSource_WebserviceSource());
}
if (isset($types['wiki page'])) {
$aggregator->addContentSource('wiki page', new Search_ContentSource_WikiSource());
}
// Global Sources
if ($prefs['feature_categories'] == 'y') {
$aggregator->addGlobalSource(new Search_GlobalSource_CategorySource());
$aggregator->addContentSource('category', new Search_ContentSource_CategorySource());
}
if ($prefs['feature_freetags'] == 'y') {
$aggregator->addGlobalSource(new Search_GlobalSource_FreeTagSource());
}
if ($prefs['rating_advanced'] == 'y' && $mode == 'indexing') {
$aggregator->addGlobalSource(new Search_GlobalSource_AdvancedRatingSource($prefs['rating_recalculation'] == 'indexing'));
}
$aggregator->addGlobalSource(new Search_GlobalSource_Geolocation());
if ($prefs['feature_search_show_visit_count'] === 'y') {
$aggregator->addGlobalSource(new Search_GlobalSource_VisitsSource());
}
if ($prefs['feature_friends'] === 'y') {
$aggregator->addGlobalSource(new Search_GlobalSource_SocialSource());
}
if ($mode == 'indexing') {
$aggregator->addGlobalSource(new Search_GlobalSource_PermissionSource(Perms::getInstance()));
$aggregator->addGlobalSource(new Search_GlobalSource_RelationSource());
}
$aggregator->addGlobalSource(new Search_GlobalSource_TitleInitialSource());
$aggregator->addGlobalSource(new Search_GlobalSource_SearchableSource());
$aggregator->addGlobalSource(new Search_GlobalSource_UrlSource());
}
/**
* @return Search_Index_Interface
*/
public function getIndex($indexType = 'data', $useCache = true)
{
global $prefs, $tiki_p_admin;
if (isset($this->indices[$indexType]) && $useCache) {
return $this->indices[$indexType];
}
$writeMode = false;
if ($indexType == 'data-write') {
$indexType = 'data';
$writeMode = true;
}
$engine = $prefs['unified_engine'];
$fallbackMySQL = false;
if ($engine == 'elastic' && $index = $this->getIndexLocation($indexType)) {
$connection = $this->getElasticConnection($writeMode);
if ($connection->getStatus()->status === 200) {
$index = new Search_Elastic_Index($connection, $index);
$index->setCamelCaseEnabled($prefs['unified_elastic_camel_case'] == 'y');
$index->setPossessiveStemmerEnabled($prefs['unified_elastic_possessive_stemmer'] == 'y');
$index->setFacetCount($prefs['search_facet_default_amount']);
if ($useCache) {
$this->indices[$indexType] = $index;
}
return $index;
}
if ($prefs['unified_elastic_mysql_search_fallback'] === 'y') {
$fallbackMySQL = true;
$prefs['unified_incremental_update'] = 'n';
}
}
if (($engine == 'mysql' || $fallbackMySQL) && $index = $this->getIndexLocation($indexType, 'mysql')) {
$index = new Search_MySql_Index(TikiDb::get(), $index);
if ($useCache) {
$this->indices[$indexType] = $index;
}
return $index;
}
// Do nothing, provide a fake index.
if ($tiki_p_admin != 'y') {
Feedback::error(tr('Contact the site administrator. The index needs rebuilding.'));
}
return new Search_Index_Memory();
}
/**
* Return the number of documents created in the last rebuild.
* @param string $index Index name
* @return int
*/
public function getLastRebuildDocsCount($index = 'default')
{
global $tikilib;
$lastStats = $tikilib->get_preference('unified_last_rebuild_stats', [], true);
if (! isset($lastStats[$index])) {
return 0;
}
return array_sum($lastStats[$index]['counts']);
}
public function getEngineInfo()
{
global $prefs;
switch ($prefs['unified_engine']) {
case 'mysql':
return $this->getMySqlEngineInfo();
case 'elastic':
$info = [];
try {
$connection = $this->getElasticConnection(true);
$root = $connection->rawApi('/');
$info[tr('Client Node')] = $root->name;
$info[tr('Elasticsearch Version')] = $root->version->number;
$info[tr('Lucene Version')] = $root->version->lucene_version;
$cluster = $connection->rawApi('/_cluster/health');
$info[tr('Cluster Name')] = $cluster->cluster_name;
$info[tr('Cluster Status')] = $cluster->status;
$info[tr('Cluster Node Count')] = $cluster->number_of_nodes;
if (version_compare($root->version->number, '1.0.0') === -1) {
$status = $connection->rawApi('/_status');
foreach ($status->indices as $indexName => $data) {
if (strpos($indexName, $prefs['unified_elastic_index_prefix']) === 0) {
$info[tr('Index %0', $indexName)] = tr(
'%0 documents, totaling %1',
$data->docs->num_docs,
$data->index->primary_size
);
}
}
$nodes = $connection->rawApi('/_nodes/jvm/stats');
foreach ($nodes->nodes as $node) {
$info[tr('Node %0', $node->name)] = tr('Using %0, since %1', $node->jvm->mem->heap_used, $node->jvm->uptime);
}
} else {
$status = $connection->getIndexStatus();
foreach ($status->indices as $indexName => $data) {
if (strpos($indexName, $prefs['unified_elastic_index_prefix']) === 0) {
if (isset($data->primaries)) { // v2
$info[tr('Index %0', $indexName)] = tr(
'%0 documents, totaling %1 bytes',
$data->primaries->docs->count,
number_format($data->primaries->store->size_in_bytes)
);
} else { // v1
$info[tr('Index %0', $indexName)] = tr(
'%0 documents, totaling %1 bytes',
$data->docs->num_docs,
number_format($data->index->primary_size_in_bytes)
);
}
}
}
$nodes = $connection->rawApi('/_nodes/stats');
foreach ($nodes->nodes as $node) {
$info[tr('Node %0', $node->name)] = tr('Using %0 bytes, since %1', number_format($node->jvm->mem->heap_used_in_bytes), date('Y-m-d H:i:s', $node->jvm->timestamp / 1000));
}
$field_count_on_last_rebuild = $prefs['unified_total_fields'] ?? $prefs['unified_field_count'] ?? null;
if (! is_null($field_count_on_last_rebuild)) {
$info[tr('Field Count Tried on Last Rebuild')] = $field_count_on_last_rebuild;
if ($field_count_on_last_rebuild > $prefs['unified_elastic_field_limit']) {
$info[tr('Warning')] = tr('Field limit setting is lower than Tiki needs to store in the index!');
}
}
}
} catch (Search_Elastic_Exception $e) {
$info[tr('Information Missing')] = $e->getMessage();
}
if ($prefs['unified_elastic_mysql_search_fallback'] === 'y') {
$info = array_merge($info, $this->getMySqlEngineInfo());
}
return $info;
default:
return [];
}
}
/**
* Retrieve Mysql Engine Stats
* @return array
*/
private function getMySqlEngineInfo(): array
{
global $tikilib;
$info = [];
$totalDocuments = $this->getLastRebuildDocsCount();
list($engine, $version) = $this->getCurrentEngineDetails();
if (! empty($version)) {
$info['MySQL Version'] = $version;
}
$query = "SELECT table_name as tbl_name, table_rows as tbl_rows FROM information_schema.tables " .
"WHERE table_schema = DATABASE() AND table_name like 'index_%'";
$indexResult = TikiDb::get()->query($query)->result;
foreach ($indexResult as $index) {
$indexName = $index['tbl_name'] ?: '';
if (preg_match('/index_([a-z1-9]+$)/', $indexName)) {
$info[tr('MySQL Index %0', $indexName)] = tr(
'%0 documents, using %1 of %2 indexes',
$totalDocuments,
count(TikiDb::get()->fetchAll("SHOW INDEXES FROM $indexName")),
Search_MySql_Table::MAX_MYSQL_INDEXES_PER_TABLE
);
continue;
}
$info[tr('MySQL Index %0', $indexName)] = tr('%0 documents', $index['tbl_rows'] ?: 0);
}
$lastRebuild = $tikilib->get_preference('unified_last_rebuild');
if (! empty($lastRebuild)) {
$info['MySQL Last Rebuild Index'] = $tikilib->get_long_date($lastRebuild) . ', ' . $tikilib->get_long_time($lastRebuild);
}
return $info;
}
public function getElasticIndexInfo($indexName)
{
$connection = $this->getElasticConnection(false);
try {
$mapping = $connection->rawApi("/$indexName/_mapping");
return $mapping;
} catch (Search_Elastic_Exception $e) {
return false;
}
}
private function getElasticConnection($useMasterOnly)
{
global $prefs;
static $connections = [];
$target = $prefs['unified_elastic_url'];
if (! $useMasterOnly && $prefs['federated_elastic_url']) {
$target = $prefs['federated_elastic_url'];
}
if (! empty($connections[$target])) {
return $connections[$target];
}
$connection = new Search_Elastic_Connection($target);
$connection->startBulk();
$connection->persistDirty(TikiLib::events());
$connections[$target] = $connection;
return $connection;
}
/**
* @param string $mode
* @return Search_Formatter_DataSource_Interface
*/
public function getDataSource($mode = 'formatting')
{
global $prefs;
$dataSource = new Search_Formatter_DataSource_Declarative();
$this->addSources($dataSource, $mode);
if ($mode === 'formatting') {
if ($prefs['unified_engine'] === 'mysql') {
$dataSource->setPrefilter(
function ($fields, $entry) {
return (new Search_MySql_Prefilter())->get($fields, $entry);
}
);
} elseif ($prefs['unified_engine'] === 'elastic') {
$connection = $this->getElasticConnection(false);
if ($connection->getStatus()->status === 200) {
$dataSource->setPrefilter(function ($fields, $entry) {
return (new Search_Elastic_Prefilter())->get($fields, $entry);
});
}
}
// If prefilter was not loaded, the main search engine might not be working properly, lets use the fallback one if possible
if (! $dataSource->isPrefilterSet()) {
if ($prefs['unified_elastic_mysql_search_fallback'] === 'y') {
$dataSource->setPrefilter(
function ($fields, $entry) {
return (new Search_MySql_Prefilter())->get($fields, $entry);
}
);
}
}
}
return $dataSource;
}
public function getProfileExportHelper()
{
$helper = new Tiki_Profile_Writer_SearchFieldHelper();
$this->addSources($helper, 'indexing'); // Need all fields, so use indexing
return $helper;
}
/**
* @return Search_Query_WeightCalculator_Field
*/
public function getWeightCalculator()
{
global $prefs;
$lines = explode("\n", $prefs['unified_field_weight']);
$weights = [];
foreach ($lines as $line) {
$parts = explode(':', $line, 2);
if (count($parts) == 2) {
$parts = array_map('trim', $parts);
$weights[$parts[0]] = $parts[1];
}
}
return new Search_Query_WeightCalculator_Field($weights);
}
public function initQuery(Search_Query $query)
{
$this->initQueryBase($query);
$this->initQueryPermissions($query);
$this->initQueryPresentation($query);
}
public function initQueryBase($query, $applyJail = true)
{
global $prefs;
$query->setWeightCalculator($this->getWeightCalculator());
$query->setIdentifierFields($prefs['unified_identifier_fields']);
$categlib = TikiLib::lib('categ');
if ($applyJail && $jail = $categlib->get_jail(false)) {
$query->filterCategory(implode(' or ', $jail), true);
}
}
public function initQueryPermissions($query)
{
global $user;
if (! Perms::get()->admin) {
$query->filterPermissions(Perms::get()->getGroups(), $user);
}
}
public function initQueryPresentation($query)
{
$query->applyTransform(new Search_Formatter_Transform_DynamicLoader($this->getDataSource('formatting')));
}
/**
* @param array $filter
* @return Search_Query
*/
public function buildQuery(array $filter, $query = null)
{
if (! $query) {
$query = new Search_Query();
$this->initQuery($query);
}
if (! is_array($filter)) {
throw new Exception('Invalid filter type provided in query. It must be an array.');
}
if (isset($filter['type']) && $filter['type']) {
$query->filterType($filter['type']);
}
if (isset($filter['categories']) && $filter['categories']) {
$query->filterCategory($filter['categories'], isset($filter['deep']));
}
if (isset($filter['tags']) && $filter['tags']) {
$query->filterTags($filter['tags']);
}
if (isset($filter['content']) && $filter['content']) {
$o = TikiLib::lib('tiki')->get_preference('unified_default_content', ['contents'], true);
if (count($o) == 1 && empty($o[0])) {
// Use "contents" field by default, if no default is specified
$query->filterContent($filter['content'], ['contents']);
} else {
$query->filterContent($filter['content'], $o);
}
}
if (isset($filter['autocomplete']) && $filter['autocomplete']) {
$query->filterInitial($filter['autocomplete']);
}
if (isset($filter['language']) && $filter['language']) {
$q = $filter['language'];
if (preg_match('/^\w+\-\w+$/', $q)) {
$q = "\"$q\"";
}
if (isset($filter['language_unspecified'])) {
$q = "($q) or unknown";
}
$query->filterLanguage($q);
}
if (isset($filter['groups'])) {
$query->filterMultivalue($filter['groups'], 'groups');
}
if (isset($filter['prefix']) && is_array($filter['prefix'])) {
foreach ($filter['prefix'] as $field => $prefix) {
$query->filterInitial((string) $prefix, $field);
}
unset($filter['prefix']);
}
if (isset($filter['not_prefix']) && is_array($filter['not_prefix'])) {
foreach ($filter['not_prefix'] as $field => $prefix) {
$query->filterNotInitial((string) $prefix, $field);
}
unset($filter['not_prefix']);
}
if (
isset($filter['distance']) && is_array($filter['distance']) &&
isset($filter['distance']['distance'], $filter['distance']['lat'], $filter['distance']['lon'])
) {
$query->filterDistance($filter['distance']['distance'], $filter['distance']['lat'], $filter['distance']['lon']);
unset($filter['distance']);
}
if (isset($filter['range']) && is_array($filter['range']) && isset($filter['range']['from'], $filter['range']['to'])) {
$field = isset($filter['range']['field']) ? $filter['range']['field'] : 'date';
$query->filterRange($filter['range']['from'], $filter['range']['to'], $field);
unset($filter['range']);
}
unset($filter['type']);
unset($filter['categories']);
unset($filter['deep']);
unset($filter['tags']);
unset($filter['content']);
unset($filter['language']);
unset($filter['language_unspecified']);
unset($filter['autocomplete']);
unset($filter['groups']);
foreach ($filter as $key => $value) {
if ($value) {
$query->filterContent($value, $key);
}
}
return $query;
}
public function getFacetProvider()
{
global $prefs;
$types = $this->getSupportedTypes();
$facets = [
Search_Query_Facet_Term::fromField('object_type')
->setLabel(tr('Object Type'))
->setRenderMap($types),
];
if ($prefs['feature_multilingual'] == 'y') {
$facets[] = Search_Query_Facet_Term::fromField('language')
->setLabel(tr('Language'))
->setRenderMap(TikiLib::lib('language')->get_language_map());
}
if ($prefs['search_date_facets'] == 'y') {
$facets[] = Search_Query_Facet_DateHistogram::fromField('date')
->setName(tr('date_histogram'))
->setLabel(tr('Date Histogram'))
->setInterval($prefs['search_date_facets_interval'])
->setRenderCallback(function ($date) {
$out = TikiLib::lib('tiki')->get_short_date($date / 1000);
return $out;
});
if ($prefs['search_date_facets_ranges']) {
$facet = Search_Query_Facet_DateRange::fromField('date')
->setName(tr('date_range'))
->setLabel(tr('Date Range'))
->setRenderCallback(function ($label) {
return $label;
});
$ranges = explode("\n", $prefs['search_date_facets_ranges']);
foreach (array_filter($ranges) as & $range) {
$range = explode(',', $range);
if (count($range) > 2) {
$facet->addRange($range[1], $range[0], $range[2]);
} elseif (count($range) > 1) {
$facet->addRange($range[1], $range[0]);
}
}
$facets[] = $facet;
}
}
if ($prefs['federated_enabled'] === 'y') {
$tiki_extwiki = TikiDb::get()->table('tiki_extwiki');
$indexMap = [
$this->getIndexLocation() => tr('Local Search'),
];
foreach (TikiLib::lib('federatedsearch')->getIndices() as $indexname => $index) {
$indexMap[$indexname] = $tiki_extwiki->fetchOne('name', [
'indexname' => $indexname,
]);
}
$facets[] = Search_Query_Facet_Term::fromField('_index')
->setLabel(tr('Federated Search'))
->setRenderCallback(function ($index) use (&$indexMap) {
$out = tr('Index not found');
if (isset($indexMap[$index])) {
$out = $indexMap[$index];
} else {
foreach ($indexMap as $candidate => $name) {
if (0 === strpos($index, $candidate . '_')) {
$indicesMap[$index] = $name;
$out = $name;
break;
}
}
}
return $out;
});
}
$provider = new Search_FacetProvider();
$provider->addFacets($facets);
$this->addSources($provider);
return $provider;
}
public function getRawArray($document)
{
return array_map(function ($entry) {
if (is_object($entry)) {
if (method_exists($entry, 'getRawValue')) {
return $entry->getRawValue();
} else {
return $entry->getValue();
}
} else {
return $entry;
}
}, $document);
}
public function isOutdated()
{
global $prefs;
// If incremental update is enabled we cannot rely on the unified_last_rebuild date.
if ($prefs['feature_search'] == 'n' || $prefs['unified_incremental_update'] == 'y') {
return false;
}
$tikilib = TikiLib::lib('tiki');
$last_rebuild = $tikilib->get_preference('unified_last_rebuild');
$threshold = strtotime('+ ' . $prefs['search_index_outdated'] . ' days', $last_rebuild);
$types = $this->getSupportedTypes();
// Content Sources
if (isset($types['wiki page'])) {
$last_page = $tikilib->list_pages(0, 1, 'lastModif_desc', '', '', true, false, false, false);
if (! empty($last_page['data'][0]['lastModif']) && $last_page['data'][0]['lastModif'] > $threshold) {
return true;
}
}
if (isset($types['forum post'])) {
$commentslib = TikiLib::lib('comments');
$last_forum_post = $commentslib->get_all_comments('forum', 0, -1, 'commentDate_desc');
if (! empty($last_forum_post['data'][0]['commentDate']) && $last_forum_post['data'][0]['commentDate'] > $threshold) {
return true;
}
$last_forum = $commentslib->list_forums(0, 1, 'created_desc');
if (! empty($last_forum['data'][0]['created']) && $last_forum['data'][0]['created'] > $threshold) {
return true;
}
}
if (isset($types['blog post'])) {
$last_blog_post = Tikilib::lib('blog')->list_blog_posts(0, false, 0, 1, 'lastModif_desc');
if (! empty($last_blog_post['data'][0]['lastModif']) && $last_blog_post['data'][0]['lastModif'] > $threshold) {
return true;
}
}
if (isset($types['article'])) {
$last_article = Tikilib::lib('art')->list_articles(0, 1, 'lastModif_desc');
if (! empty($last_article['data'][0]['lastModif']) && $last_article['data'][0]['lastModif'] > $threshold) {
return true;
}
}
if (isset($types['file'])) {
// todo: files are indexed automatically, probably nothing to do here.
}
if (isset($types['trackeritem'])) {
$trackerlib = TikiLib::lib('trk');
$last_tracker_item = $trackerlib->list_tracker_items(-1, 0, 1, 'lastModif_desc', null);
if (! empty($last_tracker_item['data'][0]['lastModif']) && $last_tracker_item['data'][0]['lastModif'] > $threshold) {
return true;
}
$last_tracker = $trackerlib->list_trackers(0, 1, 'lastModif_desc');
if (! empty($last_tracker['data'][0]['lastModif']) && $last_tracker['data'][0]['lastModif'] > $threshold) {
return true;
}
// todo: Missing tracker_fields
}
if (isset($types['sheet'])) {
$sheetlib = TikiLib::lib('sheet');
$last_sheet = $sheetlib->list_sheets(0, 1, 'begin_desc');
if (! empty($last_sheet['data'][0]['begin']) && $last_sheet['data'][0]['begin'] > $threshold) {
return true;
}
}
if (isset($types['comment'])) {
$commentTypes = [];
if ($prefs['feature_wiki_comments'] == 'y') {
$commentTypes[] = 'wiki page';
}
if ($prefs['feature_article_comments'] == 'y') {
$commentTypes[] = 'article';
}
if ($prefs['feature_poll_comments'] == 'y') {
$commentTypes[] = 'poll';
}
if ($prefs['feature_file_galleries_comments'] == 'y') {
$commentTypes[] = 'file gallery';
}
if ($prefs['feature_trackers'] == 'y') {
$commentTypes[] = 'trackeritem';
}
$commentslib = TikiLib::lib('comments');
$last_comment = $commentslib->get_all_comments($commentTypes, 0, 1, 'commentDate_desc');
if (! empty($last_comment['data'][0]['commentDate']) && $last_comment['data'][0]['commentDate'] > $threshold) {
return true;
}
}
if (isset($types['user'])) {
$userlib = TikiLib::lib('user');
$last_user = $userlib->get_users(0, 1, 'created_desc');
if (! empty($last_user['data'][0]['created']) && $last_user['data'][0]['created'] > $threshold) {
return true;
}
}
if (isset($types['group'])) {
// todo: unable to track groups by dates
}
}
/**
* Provide the name of the log file
*
* @param int $rebuildType 0: no log, 1: browser rebuild, 2: console rebuild
* @return string
*/
public function getLogFilename($rebuildType = 0, $engine = ''): string
{
global $prefs;
$logName = 'Search_Indexer';
if (empty($engine)) {
$engine = $prefs['unified_engine'];
}
switch ($engine) {
case 'elastic':
$logName .= '_elastic_' . rtrim($prefs['unified_elastic_index_prefix'], '_');
break;
case 'mysql':
$logName .= '_mysql_' . TikiDb::get()->getOne('SELECT DATABASE()');
break;
}
if ($rebuildType == 2) {
$logName .= '_console';
}
$logName = $prefs['tmpDir'] . (substr($prefs['tmpDir'], -1) === '/' ? '' : '/') . $logName . '.log';
return $logName;
}
/**
* Return the fallback search engine name
*
* @return array|null
*/
public function getFallbackEngineDetails()
{
global $prefs, $tikilib;
if ($prefs['unified_engine'] == 'elastic' && $prefs['unified_elastic_mysql_search_fallback'] === 'y') {
$engine = 'mysql';
$engineName = 'MySQL';
$version = $tikilib->getMySQLVersion();
$index = $prefs['unified_mysql_index_current'];
return [$engine, $engineName, $version, $index];
}
return null;
}
/**
* Get Indexes to restore depending on the Tiki's configuration
*
* @return array
*/
private function getIndexesToRestore()
{
global $prefs;
$currentIndex = $prefs['unified_mysql_index_current'];
if (
$prefs['unified_mysql_restore_indexes'] == 'n'
|| empty($currentIndex)
) {
return [];
}
// The excluded indexes are hardcoded on the index table creation query
$indexesToRestore = TikiDb::get()->fetchAll(
"SHOW INDEXES
FROM $currentIndex
WHERE Key_name != 'PRIMARY'
AND (Key_name != 'object_type' AND Column_name != 'object_type')
AND (Key_name != 'object_type' AND Column_name != 'object_id')"
);
return $indexesToRestore;
}
/**
* Check Elasticsearch service
*
* @return array
*/
public function checkElasticsearch()
{
global $prefs;
$searchIndex = [
'error' => false,
'feedback' => '',
'connectionError' => false,
];
if ($prefs['unified_engine'] !== 'elastic') {
return $searchIndex;
}
$connection = $this->getElasticConnection(false);
$connectionStatus = $connection->getStatus();
if ($connectionStatus->status !== 200) {
$searchIndex = [
'error' => true,
'feedback' => $connectionStatus->error ?? '',
'connectionError' => true,
];
}
return $searchIndex;
}
/**
* Check MySQL index
*
* @return array
*/
public function checkMySql()
{
global $prefs, $dbs_tiki;
$searchIndex = [
'error' => false,
'feedback' => '',
];
if ($prefs['unified_engine'] !== 'mysql') {
return $searchIndex;
}
$local_php = TikiInit::getCredentialsFile();
if (file_exists($local_php)) {
include($local_php);
}
$result = TikiDb::get()->fetchMap(
'SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = ? AND TABLE_NAME LIKE ?;',
[$dbs_tiki, 'index_%']
);
if (! array_key_exists($prefs['unified_mysql_index_current'], $result)) {
$feedback = tra('MySql Search index table not found.');
$searchIndex = [
'error' => true,
'feedback' => $feedback,
];
}
return $searchIndex;
}
}