16225b270SMichael Große<?php 26225b270SMichael Große 36225b270SMichael Großenamespace dokuwiki\Search; 46225b270SMichael Große 56225b270SMichael Großeuse dokuwiki\Extension\Event; 6*4027a91aSSatoshi Saharause dokuwiki\Search\PagewordIndex; 7*4027a91aSSatoshi Saharause dokuwiki\Search\MetadataIndex; 8*4027a91aSSatoshi Sahara 9*4027a91aSSatoshi Sahara// Version tag used to force rebuild on upgrade 10*4027a91aSSatoshi Saharaconst INDEXER_VERSION = 8; 116225b270SMichael Große 126225b270SMichael Große/** 13*4027a91aSSatoshi Sahara * Class DokuWiki Indexer (Singleton) 146225b270SMichael Große * 15*4027a91aSSatoshi Sahara * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 166225b270SMichael Große * @author Andreas Gohr <andi@splitbrain.org> 17*4027a91aSSatoshi Sahara * @author Tom N Harris <tnharris@whoopdedo.org> 186225b270SMichael Große */ 19*4027a91aSSatoshi Saharaclass Indexer extends AbstractIndex 20*4027a91aSSatoshi Sahara{ 21*4027a91aSSatoshi Sahara /** @var Indexer $instance */ 22*4027a91aSSatoshi Sahara protected static $instance = null; 236225b270SMichael Große 24*4027a91aSSatoshi Sahara /** 25*4027a91aSSatoshi Sahara * Get new or existing singleton instance of the Indexer 26*4027a91aSSatoshi Sahara * 27*4027a91aSSatoshi Sahara * @return Indexer 28*4027a91aSSatoshi Sahara */ 29*4027a91aSSatoshi Sahara public static function getInstance() 30*4027a91aSSatoshi Sahara { 31*4027a91aSSatoshi Sahara if (is_null(static::$instance)) { 32*4027a91aSSatoshi Sahara static::$instance = new static(); 336225b270SMichael Große } 34*4027a91aSSatoshi Sahara return static::$instance; 356225b270SMichael Große } 366225b270SMichael Große 376225b270SMichael Große /** 38*4027a91aSSatoshi Sahara * Dispatch Indexing request for the page, called by TaskRunner::runIndexer() 396225b270SMichael Große * 40*4027a91aSSatoshi Sahara * @param string $page name of the page to index 41*4027a91aSSatoshi Sahara * @param bool $verbose print status messages 42*4027a91aSSatoshi Sahara * @param bool $force force reindexing even when the index is up to date 43*4027a91aSSatoshi Sahara * @return bool If the function completed successfully 446225b270SMichael Große * 456225b270SMichael Große * @author Tom N Harris <tnharris@whoopdedo.org> 46*4027a91aSSatoshi Sahara * @author Satoshi Sahara <sahara.satoshi@gmail.com> 476225b270SMichael Große */ 48*4027a91aSSatoshi Sahara public function dispatch($page, $verbose = false, $force = false) 49*4027a91aSSatoshi Sahara { 50*4027a91aSSatoshi Sahara // check if page was deleted but is still in the index 51*4027a91aSSatoshi Sahara if (!page_exists($page)) { 52*4027a91aSSatoshi Sahara $result = $this->deletePage($page, $verbose, $force); 53*4027a91aSSatoshi Sahara return $result; 546225b270SMichael Große } else { 55*4027a91aSSatoshi Sahara // update search index 56*4027a91aSSatoshi Sahara $result = $this->addPage($page, $verbose, $force); 57*4027a91aSSatoshi Sahara return $result; 586225b270SMichael Große } 596225b270SMichael Große } 606225b270SMichael Große 616225b270SMichael Große /** 62*4027a91aSSatoshi Sahara * Version of the indexer taking into consideration the external tokenizer. 63*4027a91aSSatoshi Sahara * The indexer is only compatible with data written by the same version. 646225b270SMichael Große * 65*4027a91aSSatoshi Sahara * @triggers INDEXER_VERSION_GET 66*4027a91aSSatoshi Sahara * Plugins that modify what gets indexed should hook this event and 67*4027a91aSSatoshi Sahara * add their version info to the event data like so: 68*4027a91aSSatoshi Sahara * $data[$plugin_name] = $plugin_version; 696225b270SMichael Große * 706225b270SMichael Große * @author Tom N Harris <tnharris@whoopdedo.org> 716225b270SMichael Große * @author Michael Hamann <michael@content-space.de> 72*4027a91aSSatoshi Sahara * 73*4027a91aSSatoshi Sahara * @return int|string 746225b270SMichael Große */ 75*4027a91aSSatoshi Sahara public function getVersion() 76*4027a91aSSatoshi Sahara { 77*4027a91aSSatoshi Sahara static $indexer_version = null; 78*4027a91aSSatoshi Sahara if ($indexer_version == null) { 79*4027a91aSSatoshi Sahara $version = INDEXER_VERSION; 80*4027a91aSSatoshi Sahara 81*4027a91aSSatoshi Sahara // DokuWiki version is included for the convenience of plugins 82*4027a91aSSatoshi Sahara $data = array('dokuwiki' => $version); 83*4027a91aSSatoshi Sahara Event::createAndTrigger('INDEXER_VERSION_GET', $data, null, false); 84*4027a91aSSatoshi Sahara unset($data['dokuwiki']); // this needs to be first 85*4027a91aSSatoshi Sahara ksort($data); 86*4027a91aSSatoshi Sahara foreach ($data as $plugin => $vers) { 87*4027a91aSSatoshi Sahara $version .= '+'.$plugin.'='.$vers; 88*4027a91aSSatoshi Sahara } 89*4027a91aSSatoshi Sahara $indexer_version = $version; 90*4027a91aSSatoshi Sahara } 91*4027a91aSSatoshi Sahara return $indexer_version; 926225b270SMichael Große } 936225b270SMichael Große 94*4027a91aSSatoshi Sahara /** 95*4027a91aSSatoshi Sahara * Adds/updates the search index for the given page 96*4027a91aSSatoshi Sahara * 97*4027a91aSSatoshi Sahara * Locking is handled internally. 98*4027a91aSSatoshi Sahara * 99*4027a91aSSatoshi Sahara * @param string $page name of the page to index 100*4027a91aSSatoshi Sahara * @param bool $verbose print status messages 101*4027a91aSSatoshi Sahara * @param bool $force force reindexing even when the index is up to date 102*4027a91aSSatoshi Sahara * @return bool If the function completed successfully 103*4027a91aSSatoshi Sahara * 104*4027a91aSSatoshi Sahara * @author Tom N Harris <tnharris@whoopdedo.org> 105*4027a91aSSatoshi Sahara * @author Satoshi Sahara <sahara.satoshi@gmail.com> 106*4027a91aSSatoshi Sahara */ 107*4027a91aSSatoshi Sahara public function addPage($page, $verbose = false, $force = false) 108*4027a91aSSatoshi Sahara { 109*4027a91aSSatoshi Sahara // check if indexing needed for the existing page (full text and/or metadata indexing) 110*4027a91aSSatoshi Sahara $idxtag = metaFN($page,'.indexed'); 111*4027a91aSSatoshi Sahara if (!$force && file_exists($idxtag)) { 112*4027a91aSSatoshi Sahara if (trim(io_readFile($idxtag)) == $this->getVersion()) { 113*4027a91aSSatoshi Sahara $last = @filemtime($idxtag); 114*4027a91aSSatoshi Sahara if ($last > @filemtime(wikiFN($page))) { 115*4027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: index for {$page} up to date"); 116*4027a91aSSatoshi Sahara return true; 117*4027a91aSSatoshi Sahara } 118*4027a91aSSatoshi Sahara } 119*4027a91aSSatoshi Sahara } 1206225b270SMichael Große 121*4027a91aSSatoshi Sahara // register the page to the page.idx 122*4027a91aSSatoshi Sahara $pid = $this->getPID($page); 1236225b270SMichael Große if ($pid === false) { 124*4027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: getting the PID failed for {$page}"); 125*4027a91aSSatoshi Sahara trigger_error("Failed to get PID for {$page}", E_USER_ERROR); 1266225b270SMichael Große return false; 1276225b270SMichael Große } 1286225b270SMichael Große 129*4027a91aSSatoshi Sahara // prepare metadata indexing 130*4027a91aSSatoshi Sahara $metadata = array(); 131*4027a91aSSatoshi Sahara $metadata['title'] = p_get_metadata($page, 'title', METADATA_RENDER_UNLIMITED); 1326225b270SMichael Große 133*4027a91aSSatoshi Sahara $references = p_get_metadata($page, 'relation references', METADATA_RENDER_UNLIMITED); 134*4027a91aSSatoshi Sahara $metadata['relation_references'] = ($references !== null) ? 135*4027a91aSSatoshi Sahara array_keys($references) : array(); 1366225b270SMichael Große 137*4027a91aSSatoshi Sahara $media = p_get_metadata($page, 'relation media', METADATA_RENDER_UNLIMITED); 138*4027a91aSSatoshi Sahara $metadata['relation_media'] = ($media !== null) ? 139*4027a91aSSatoshi Sahara array_keys($media) : array(); 1406225b270SMichael Große 141*4027a91aSSatoshi Sahara // check if full text indexing allowed 142*4027a91aSSatoshi Sahara $indexenabled = p_get_metadata($page, 'internal index', METADATA_RENDER_UNLIMITED); 143*4027a91aSSatoshi Sahara if ($indexenabled !== false) $indexenabled = true; 144*4027a91aSSatoshi Sahara $metadata['internal_index'] = $indexenabled; 1456225b270SMichael Große 146*4027a91aSSatoshi Sahara $body = ''; 147*4027a91aSSatoshi Sahara $data = compact('page', 'body', 'metadata', 'pid'); 148*4027a91aSSatoshi Sahara $event = new Event('INDEXER_PAGE_ADD', $data); 149*4027a91aSSatoshi Sahara if ($event->advise_before()) $data['body'] = $data['body'].' '.rawWiki($page); 150*4027a91aSSatoshi Sahara $event->advise_after(); 151*4027a91aSSatoshi Sahara unset($event); 152*4027a91aSSatoshi Sahara extract($data); 153*4027a91aSSatoshi Sahara $indexenabled = $metadata['internal_index']; 154*4027a91aSSatoshi Sahara unset($metadata['internal_index']); 1556225b270SMichael Große 156*4027a91aSSatoshi Sahara // Access to Metadata Index 157*4027a91aSSatoshi Sahara $MetadataIndex = MetadataIndex::getInstance(); 158*4027a91aSSatoshi Sahara $result = $MetadataIndex->addMetaKeys($page, $metadata); 159*4027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: addMetaKeys({$page}) ".($result ? 'done' : 'failed')); 160*4027a91aSSatoshi Sahara if (!$result) { 1616225b270SMichael Große return false; 1626225b270SMichael Große } 1636225b270SMichael Große 164*4027a91aSSatoshi Sahara // Access to Pageword Index 165*4027a91aSSatoshi Sahara $PagewordIndex = PagewordIndex::getInstance(); 166*4027a91aSSatoshi Sahara if ($indexenabled) { 167*4027a91aSSatoshi Sahara $result = $PagewordIndex->addPageWords($page, $body); 168*4027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: addPageWords({$page}) ".($result ? 'done' : 'failed')); 169*4027a91aSSatoshi Sahara if (!$result) { 1706225b270SMichael Große return false; 1716225b270SMichael Große } 1726225b270SMichael Große } else { 173*4027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: full text indexing disabled for {$page}"); 174*4027a91aSSatoshi Sahara // ensure the page content deleted from the pageword index 175*4027a91aSSatoshi Sahara $result = $PagewordIndex->deletePageWords($page); 176*4027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: deletePageWords({$page}) ".($result ? 'done' : 'failed')); 177*4027a91aSSatoshi Sahara if (!$result) { 1786225b270SMichael Große return false; 1796225b270SMichael Große } 1806225b270SMichael Große } 1816225b270SMichael Große 182*4027a91aSSatoshi Sahara // update index tag file 183*4027a91aSSatoshi Sahara io_saveFile($idxtag, $this->getVersion()); 184*4027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: finished"); 185*4027a91aSSatoshi Sahara 186*4027a91aSSatoshi Sahara return $result; 1876225b270SMichael Große } 1886225b270SMichael Große 1896225b270SMichael Große /** 1906225b270SMichael Große * Remove a page from the index 1916225b270SMichael Große * 192*4027a91aSSatoshi Sahara * Erases entries in all known indexes. Locking is handled internally. 1936225b270SMichael Große * 194*4027a91aSSatoshi Sahara * @param string $page name of the page to index 195*4027a91aSSatoshi Sahara * @param bool $verbose print status messages 196*4027a91aSSatoshi Sahara * @param bool $force force reindexing even when the index is up to date 197*4027a91aSSatoshi Sahara * @return bool If the function completed successfully 1986225b270SMichael Große * 1996225b270SMichael Große * @author Tom N Harris <tnharris@whoopdedo.org> 200*4027a91aSSatoshi Sahara * @author Satoshi Sahara <sahara.satoshi@gmail.com> 2016225b270SMichael Große */ 202*4027a91aSSatoshi Sahara public function deletePage($page, $verbose = false, $force = false) 203*4027a91aSSatoshi Sahara { 204*4027a91aSSatoshi Sahara $idxtag = metaFN($page,'.indexed'); 205*4027a91aSSatoshi Sahara if (!$force && !file_exists($idxtag)) { 206*4027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: {$page}.indexed file does not exist, ignoring"); 207*4027a91aSSatoshi Sahara return true; 208*4027a91aSSatoshi Sahara } 2096225b270SMichael Große 210*4027a91aSSatoshi Sahara // remove obsoleted content from pageword index 211*4027a91aSSatoshi Sahara $PagewordIndex = PagewordIndex::getInstance(); 212*4027a91aSSatoshi Sahara $result = $PagewordIndex->deletePageWords($page); 213*4027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: deletePageWords({$page}) ".($result ? 'done' : 'failed')); 214*4027a91aSSatoshi Sahara if (!$result) { 215*4027a91aSSatoshi Sahara return false; 216*4027a91aSSatoshi Sahara } 2176225b270SMichael Große 218*4027a91aSSatoshi Sahara // delete all keys of the page from metadata index 219*4027a91aSSatoshi Sahara $MetadataIndex = MetadataIndex::getInstance(); 220*4027a91aSSatoshi Sahara $result = $MetadataIndex->deleteMetaKeys($page); 221*4027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: deleteMetaKeys({$page}) ".($result ? 'done' : 'failed')); 222*4027a91aSSatoshi Sahara if (!$result) { 223*4027a91aSSatoshi Sahara return false; 224*4027a91aSSatoshi Sahara } 225*4027a91aSSatoshi Sahara 226*4027a91aSSatoshi Sahara // mark the page as deleted in the page.idx 227*4027a91aSSatoshi Sahara $pid = $this->getPID($page); 228*4027a91aSSatoshi Sahara if ($pid !== false) { 229*4027a91aSSatoshi Sahara if (!$this->lock()) return false; // set $errors property 230*4027a91aSSatoshi Sahara $result = $this->saveIndexKey('page', '', $pid, '#deleted:'.$page); 231*4027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: update page.idx ".($result ? 'done' : 'failed')); 2326225b270SMichael Große $this->unlock(); 233*4027a91aSSatoshi Sahara } else { 234*4027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: {$page} not found in the page.idx, ignoring"); 235*4027a91aSSatoshi Sahara $resullt = true; 236*4027a91aSSatoshi Sahara } 237*4027a91aSSatoshi Sahara 238*4027a91aSSatoshi Sahara unset(static::$pidCache[$pid]); 239*4027a91aSSatoshi Sahara @unlink($idxtag); 240*4027a91aSSatoshi Sahara return $result; 241*4027a91aSSatoshi Sahara } 242*4027a91aSSatoshi Sahara 243*4027a91aSSatoshi Sahara /** 244*4027a91aSSatoshi Sahara * Rename a page in the search index without changing the indexed content. 245*4027a91aSSatoshi Sahara * This function doesn't check if the old or new name exists in the filesystem. 246*4027a91aSSatoshi Sahara * It returns an error if the old page isn't in the page list of the indexer 247*4027a91aSSatoshi Sahara * and it deletes all previously indexed content of the new page. 248*4027a91aSSatoshi Sahara * 249*4027a91aSSatoshi Sahara * @param string $oldpage The old page name 250*4027a91aSSatoshi Sahara * @param string $newpage The new page name 251*4027a91aSSatoshi Sahara * @return bool If the page was successfully renamed 252*4027a91aSSatoshi Sahara */ 253*4027a91aSSatoshi Sahara public function renamePage($oldpage, $newpage) 254*4027a91aSSatoshi Sahara { 255*4027a91aSSatoshi Sahara $index = $this->getIndex('page', ''); 256*4027a91aSSatoshi Sahara // check if oldpage found in page.idx 257*4027a91aSSatoshi Sahara $oldPid = array_search($oldpage, $index, true); 258*4027a91aSSatoshi Sahara if ($oldPid === false) return false; 259*4027a91aSSatoshi Sahara 260*4027a91aSSatoshi Sahara // check if newpage found in page.idx 261*4027a91aSSatoshi Sahara $newPid = array_search($newpage, $index, true); 262*4027a91aSSatoshi Sahara if ($newPid !== false) { 263*4027a91aSSatoshi Sahara $result = $this->deletePage($newpage); 264*4027a91aSSatoshi Sahara if (!$result) return false; 265*4027a91aSSatoshi Sahara // Note: $index is no longer valid after deletePage()! 266*4027a91aSSatoshi Sahara unset($index); 267*4027a91aSSatoshi Sahara } 268*4027a91aSSatoshi Sahara 269*4027a91aSSatoshi Sahara // update page.idx 270*4027a91aSSatoshi Sahara if (!$this->lock()) return false; // set $errors property 271*4027a91aSSatoshi Sahara $result = $this->saveIndexKey('page', '', $oldPid, $newpage); 272*4027a91aSSatoshi Sahara $this->unlock(); 273*4027a91aSSatoshi Sahara 274*4027a91aSSatoshi Sahara // reset the pid cache 275*4027a91aSSatoshi Sahara $this->resetPIDCache(); 2766225b270SMichael Große 2776225b270SMichael Große return $result; 2786225b270SMichael Große } 2796225b270SMichael Große 2806225b270SMichael Große /** 281*4027a91aSSatoshi Sahara * Clear the Page Index 2826225b270SMichael Große * 283*4027a91aSSatoshi Sahara * @param bool $requireLock 2846225b270SMichael Große * @return bool If the index has been cleared successfully 2856225b270SMichael Große */ 286*4027a91aSSatoshi Sahara public function clear($requireLock = true) 287*4027a91aSSatoshi Sahara { 2886225b270SMichael Große global $conf; 2896225b270SMichael Große 290*4027a91aSSatoshi Sahara if ($requireLock && !$this->lock()) return false; 291*4027a91aSSatoshi Sahara 292*4027a91aSSatoshi Sahara // clear Metadata Index 293*4027a91aSSatoshi Sahara $MetadataIndex = MetadataIndex::getInstance(); 294*4027a91aSSatoshi Sahara $MetadataIndex->clear(false); 295*4027a91aSSatoshi Sahara 296*4027a91aSSatoshi Sahara // clear Pageword Index 297*4027a91aSSatoshi Sahara $PagewordIndex = PagewordIndex::getInstance(); 298*4027a91aSSatoshi Sahara $PagewordIndex->clear(false); 2996225b270SMichael Große 3006225b270SMichael Große @unlink($conf['indexdir'].'/page.idx'); 3016225b270SMichael Große 3026225b270SMichael Große // clear the pid cache 303*4027a91aSSatoshi Sahara $this->resetPIDCache(); 3046225b270SMichael Große 305*4027a91aSSatoshi Sahara if ($requireLock) $this->unlock(); 3066225b270SMichael Große return true; 3076225b270SMichael Große } 3086225b270SMichael Große 3096225b270SMichael Große 3106225b270SMichael Große /** 3116225b270SMichael Große * Return a list of words sorted by number of times used 3126225b270SMichael Große * 3136225b270SMichael Große * @param int $min bottom frequency threshold 3146225b270SMichael Große * @param int $max upper frequency limit. No limit if $max<$min 3156225b270SMichael Große * @param int $minlen minimum length of words to count 3166225b270SMichael Große * @param string $key metadata key to list. Uses the fulltext index if not given 3176225b270SMichael Große * @return array list of words as the keys and frequency as values 3186225b270SMichael Große * 3196225b270SMichael Große * @author Tom N Harris <tnharris@whoopdedo.org> 3206225b270SMichael Große */ 321*4027a91aSSatoshi Sahara public function histogram($min=1, $max=0, $minlen=3, $key=null) 322*4027a91aSSatoshi Sahara { 323*4027a91aSSatoshi Sahara if ($min < 1) $min = 1; 324*4027a91aSSatoshi Sahara if ($max < $min) $max = 0; 3256225b270SMichael Große 3266225b270SMichael Große $result = array(); 3276225b270SMichael Große 3286225b270SMichael Große if ($key == 'title') { 3296225b270SMichael Große $index = $this->getIndex('title', ''); 3306225b270SMichael Große $index = array_count_values($index); 3316225b270SMichael Große foreach ($index as $val => $cnt) { 332*4027a91aSSatoshi Sahara if ($cnt >= $min && (!$max || $cnt <= $max) && strlen($val) >= $minlen) { 3336225b270SMichael Große $result[$val] = $cnt; 3346225b270SMichael Große } 3356225b270SMichael Große } 336*4027a91aSSatoshi Sahara } elseif (!is_null($key)) { 337*4027a91aSSatoshi Sahara $metaname = $this->cleanName($key); 3386225b270SMichael Große $index = $this->getIndex($metaname.'_i', ''); 3396225b270SMichael Große $val_idx = array(); 3406225b270SMichael Große foreach ($index as $wid => $line) { 3416225b270SMichael Große $freq = $this->countTuples($line); 342*4027a91aSSatoshi Sahara if ($freq >= $min && (!$max || $freq <= $max)) { 3436225b270SMichael Große $val_idx[$wid] = $freq; 3446225b270SMichael Große } 345*4027a91aSSatoshi Sahara } 3466225b270SMichael Große if (!empty($val_idx)) { 3476225b270SMichael Große $words = $this->getIndex($metaname.'_w', ''); 3486225b270SMichael Große foreach ($val_idx as $wid => $freq) { 349*4027a91aSSatoshi Sahara if (strlen($words[$wid]) >= $minlen) { 3506225b270SMichael Große $result[$words[$wid]] = $freq; 3516225b270SMichael Große } 3526225b270SMichael Große } 3536225b270SMichael Große } 354*4027a91aSSatoshi Sahara } else { 355*4027a91aSSatoshi Sahara $PagewordIndex = PagewordIndex::getInstance(); 356*4027a91aSSatoshi Sahara $lengths = $PagewordIndex->listIndexLengths(); 3576225b270SMichael Große foreach ($lengths as $length) { 3586225b270SMichael Große if ($length < $minlen) continue; 3596225b270SMichael Große $index = $this->getIndex('i', $length); 3606225b270SMichael Große $words = null; 3616225b270SMichael Große foreach ($index as $wid => $line) { 3626225b270SMichael Große $freq = $this->countTuples($line); 3636225b270SMichael Große if ($freq >= $min && (!$max || $freq <= $max)) { 364*4027a91aSSatoshi Sahara if ($words === null) { 3656225b270SMichael Große $words = $this->getIndex('w', $length); 366*4027a91aSSatoshi Sahara } 3676225b270SMichael Große $result[$words[$wid]] = $freq; 3686225b270SMichael Große } 3696225b270SMichael Große } 3706225b270SMichael Große } 3716225b270SMichael Große } 3726225b270SMichael Große 3736225b270SMichael Große arsort($result); 3746225b270SMichael Große return $result; 3756225b270SMichael Große } 3766225b270SMichael Große} 377