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