16225b270SMichael Große<?php 26225b270SMichael Große 36225b270SMichael Großenamespace dokuwiki\Search; 46225b270SMichael Große 56225b270SMichael Großeuse dokuwiki\Extension\Event; 615f699acSAndreas Gohruse dokuwiki\Search\Exception\IndexAccessException; 7a16bd548SSatoshi Saharause dokuwiki\Search\Exception\IndexLockException; 8a16bd548SSatoshi Saharause dokuwiki\Search\Exception\IndexWriteException; 94027a91aSSatoshi Sahara 104027a91aSSatoshi Sahara// Version tag used to force rebuild on upgrade 114027a91aSSatoshi Saharaconst INDEXER_VERSION = 8; 126225b270SMichael Große 136225b270SMichael Große/** 14a32da6ddSSatoshi Sahara * Class DokuWiki Indexer 156225b270SMichael Große * 164027a91aSSatoshi Sahara * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 176225b270SMichael Große * @author Andreas Gohr <andi@splitbrain.org> 184027a91aSSatoshi Sahara * @author Tom N Harris <tnharris@whoopdedo.org> 196225b270SMichael Große */ 204027a91aSSatoshi Saharaclass Indexer extends AbstractIndex 214027a91aSSatoshi Sahara{ 22a32da6ddSSatoshi Sahara // page to be indexed 23a32da6ddSSatoshi Sahara protected $page; 246225b270SMichael Große 254027a91aSSatoshi Sahara /** 26a32da6ddSSatoshi Sahara * Indexer constructor 274027a91aSSatoshi Sahara * 28a32da6ddSSatoshi Sahara * @param string $page name of the page to index 294027a91aSSatoshi Sahara * @return Indexer 304027a91aSSatoshi Sahara */ 31a32da6ddSSatoshi Sahara public function __construct($page = null) 324027a91aSSatoshi Sahara { 33a32da6ddSSatoshi Sahara if (isset($page)) $this->page = $page; 346225b270SMichael Große } 356225b270SMichael Große 366225b270SMichael Große /** 374027a91aSSatoshi Sahara * Dispatch Indexing request for the page, called by TaskRunner::runIndexer() 386225b270SMichael Große * 394027a91aSSatoshi Sahara * @param bool $verbose print status messages 404027a91aSSatoshi Sahara * @param bool $force force reindexing even when the index is up to date 414027a91aSSatoshi Sahara * @return bool If the function completed successfully 426225b270SMichael Große * 43a32da6ddSSatoshi Sahara * @throws IndexAccessException 44a16bd548SSatoshi Sahara * @throws IndexLockException 45a16bd548SSatoshi Sahara * @throws IndexWriteException 464027a91aSSatoshi Sahara * @author Satoshi Sahara <sahara.satoshi@gmail.com> 4715f699acSAndreas Gohr * @author Tom N Harris <tnharris@whoopdedo.org> 486225b270SMichael Große */ 49a32da6ddSSatoshi Sahara public function dispatch($verbose = false, $force = false) 504027a91aSSatoshi Sahara { 51a32da6ddSSatoshi Sahara if (!isset($this->page)) { 52a32da6ddSSatoshi Sahara throw new IndexAccessException('Indexer: unknow page name'); 53a32da6ddSSatoshi Sahara } 54a32da6ddSSatoshi Sahara 554027a91aSSatoshi Sahara // check if page was deleted but is still in the index 56a32da6ddSSatoshi Sahara if (!page_exists($this->page)) { 57a32da6ddSSatoshi Sahara return $this->deletePage($verbose, $force); 586225b270SMichael Große } 5911d2e7d0SSatoshi Sahara 6011d2e7d0SSatoshi Sahara // update search index 61a32da6ddSSatoshi Sahara return $this->addPage($verbose, $force); 626225b270SMichael Große } 636225b270SMichael Große 646225b270SMichael Große /** 654027a91aSSatoshi Sahara * Version of the indexer taking into consideration the external tokenizer. 664027a91aSSatoshi Sahara * The indexer is only compatible with data written by the same version. 676225b270SMichael Große * 684027a91aSSatoshi Sahara * @triggers INDEXER_VERSION_GET 694027a91aSSatoshi Sahara * Plugins that modify what gets indexed should hook this event and 704027a91aSSatoshi Sahara * add their version info to the event data like so: 714027a91aSSatoshi Sahara * $data[$plugin_name] = $plugin_version; 726225b270SMichael Große * 736225b270SMichael Große * @author Tom N Harris <tnharris@whoopdedo.org> 746225b270SMichael Große * @author Michael Hamann <michael@content-space.de> 754027a91aSSatoshi Sahara * 764027a91aSSatoshi Sahara * @return int|string 776225b270SMichael Große */ 784027a91aSSatoshi Sahara public function getVersion() 794027a91aSSatoshi Sahara { 804027a91aSSatoshi Sahara static $indexer_version = null; 814027a91aSSatoshi Sahara if ($indexer_version == null) { 824027a91aSSatoshi Sahara $version = INDEXER_VERSION; 834027a91aSSatoshi Sahara 844027a91aSSatoshi Sahara // DokuWiki version is included for the convenience of plugins 854027a91aSSatoshi Sahara $data = array('dokuwiki' => $version); 864027a91aSSatoshi Sahara Event::createAndTrigger('INDEXER_VERSION_GET', $data, null, false); 874027a91aSSatoshi Sahara unset($data['dokuwiki']); // this needs to be first 884027a91aSSatoshi Sahara ksort($data); 894027a91aSSatoshi Sahara foreach ($data as $plugin => $vers) { 904027a91aSSatoshi Sahara $version .= '+'.$plugin.'='.$vers; 914027a91aSSatoshi Sahara } 924027a91aSSatoshi Sahara $indexer_version = $version; 934027a91aSSatoshi Sahara } 944027a91aSSatoshi Sahara return $indexer_version; 956225b270SMichael Große } 966225b270SMichael Große 974027a91aSSatoshi Sahara /** 984027a91aSSatoshi Sahara * Adds/updates the search index for the given page 994027a91aSSatoshi Sahara * 1004027a91aSSatoshi Sahara * Locking is handled internally. 1014027a91aSSatoshi Sahara * 1024027a91aSSatoshi Sahara * @param bool $verbose print status messages 1034027a91aSSatoshi Sahara * @param bool $force force reindexing even when the index is up to date 1044027a91aSSatoshi Sahara * @return bool If the function completed successfully 1054027a91aSSatoshi Sahara * 106a32da6ddSSatoshi Sahara * @throws IndexAccessException 107a16bd548SSatoshi Sahara * @throws IndexLockException 108a16bd548SSatoshi Sahara * @throws IndexWriteException 1094027a91aSSatoshi Sahara * @author Satoshi Sahara <sahara.satoshi@gmail.com> 11015f699acSAndreas Gohr * @author Tom N Harris <tnharris@whoopdedo.org> 1114027a91aSSatoshi Sahara */ 112a32da6ddSSatoshi Sahara public function addPage($verbose = false, $force = false) 1134027a91aSSatoshi Sahara { 114a32da6ddSSatoshi Sahara if (!isset($this->page)) { 115a32da6ddSSatoshi Sahara throw new IndexAccessException('Indexer: invalid page name in addePage'); 116a32da6ddSSatoshi Sahara } else { 117a32da6ddSSatoshi Sahara $page = $this->page; 118a32da6ddSSatoshi Sahara } 119a32da6ddSSatoshi Sahara 1204027a91aSSatoshi Sahara // check if indexing needed for the existing page (full text and/or metadata indexing) 1214027a91aSSatoshi Sahara $idxtag = metaFN($page,'.indexed'); 1224027a91aSSatoshi Sahara if (!$force && file_exists($idxtag)) { 1234027a91aSSatoshi Sahara if (trim(io_readFile($idxtag)) == $this->getVersion()) { 1244027a91aSSatoshi Sahara $last = @filemtime($idxtag); 1254027a91aSSatoshi Sahara if ($last > @filemtime(wikiFN($page))) { 1264027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: index for {$page} up to date"); 1274027a91aSSatoshi Sahara return true; 1284027a91aSSatoshi Sahara } 1294027a91aSSatoshi Sahara } 1304027a91aSSatoshi Sahara } 1316225b270SMichael Große 132*725e8e5fSSatoshi Sahara // register the page to the page.idx file, $pid is always integer 1334027a91aSSatoshi Sahara $pid = $this->getPID($page); 1346225b270SMichael Große 1354027a91aSSatoshi Sahara // prepare metadata indexing 1364027a91aSSatoshi Sahara $metadata = array(); 1374027a91aSSatoshi Sahara $metadata['title'] = p_get_metadata($page, 'title', METADATA_RENDER_UNLIMITED); 1386225b270SMichael Große 1394027a91aSSatoshi Sahara $references = p_get_metadata($page, 'relation references', METADATA_RENDER_UNLIMITED); 1404027a91aSSatoshi Sahara $metadata['relation_references'] = ($references !== null) ? 1414027a91aSSatoshi Sahara array_keys($references) : array(); 1426225b270SMichael Große 1434027a91aSSatoshi Sahara $media = p_get_metadata($page, 'relation media', METADATA_RENDER_UNLIMITED); 1444027a91aSSatoshi Sahara $metadata['relation_media'] = ($media !== null) ? 1454027a91aSSatoshi Sahara array_keys($media) : array(); 1466225b270SMichael Große 1474027a91aSSatoshi Sahara // check if full text indexing allowed 1484027a91aSSatoshi Sahara $indexenabled = p_get_metadata($page, 'internal index', METADATA_RENDER_UNLIMITED); 1494027a91aSSatoshi Sahara if ($indexenabled !== false) $indexenabled = true; 1504027a91aSSatoshi Sahara $metadata['internal_index'] = $indexenabled; 1516225b270SMichael Große 1524027a91aSSatoshi Sahara $body = ''; 1534027a91aSSatoshi Sahara $data = compact('page', 'body', 'metadata', 'pid'); 1544027a91aSSatoshi Sahara $event = new Event('INDEXER_PAGE_ADD', $data); 1554027a91aSSatoshi Sahara if ($event->advise_before()) $data['body'] = $data['body'].' '.rawWiki($page); 1564027a91aSSatoshi Sahara $event->advise_after(); 1574027a91aSSatoshi Sahara unset($event); 1584027a91aSSatoshi Sahara extract($data); 1594027a91aSSatoshi Sahara $indexenabled = $metadata['internal_index']; 1604027a91aSSatoshi Sahara unset($metadata['internal_index']); 1616225b270SMichael Große 1624027a91aSSatoshi Sahara // Access to Metadata Index 163*725e8e5fSSatoshi Sahara $result = (new MetadataIndex($pid))->addMetaKeys($metadata); 1644027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: addMetaKeys({$page}) ".($result ? 'done' : 'failed')); 1654027a91aSSatoshi Sahara if (!$result) { 1666225b270SMichael Große return false; 1676225b270SMichael Große } 1686225b270SMichael Große 169743c9a28SSatoshi Sahara // Access to Fulltext Index 1704027a91aSSatoshi Sahara if ($indexenabled) { 171*725e8e5fSSatoshi Sahara $result = (new FulltextIndex($pid))->addWords($body); 172*725e8e5fSSatoshi Sahara if ($verbose) dbglog("Indexer: addWords() for {$page} done"); 1734027a91aSSatoshi Sahara if (!$result) { 1746225b270SMichael Große return false; 1756225b270SMichael Große } 1766225b270SMichael Große } else { 1774027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: full text indexing disabled for {$page}"); 178743c9a28SSatoshi Sahara // ensure the page content deleted from the Fulltext index 179a32da6ddSSatoshi Sahara $result = (new FulltextIndex($page))->deleteWords(); 180*725e8e5fSSatoshi Sahara if ($verbose) dbglog("Indexer: deleteWords() for {$page} done"); 1814027a91aSSatoshi Sahara if (!$result) { 1826225b270SMichael Große return false; 1836225b270SMichael Große } 1846225b270SMichael Große } 1856225b270SMichael Große 1864027a91aSSatoshi Sahara // update index tag file 1874027a91aSSatoshi Sahara io_saveFile($idxtag, $this->getVersion()); 1884027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: finished"); 1894027a91aSSatoshi Sahara 1904027a91aSSatoshi Sahara return $result; 1916225b270SMichael Große } 1926225b270SMichael Große 1936225b270SMichael Große /** 1945f9bd525SSatoshi Sahara * Remove a page from the index 1956225b270SMichael Große * 1965f9bd525SSatoshi Sahara * Erases entries in all known indexes. Locking is handled internally. 1976225b270SMichael Große * 1984027a91aSSatoshi Sahara * @param string $page name of the page to index 1994027a91aSSatoshi Sahara * @param bool $verbose print status messages 2004027a91aSSatoshi Sahara * @param bool $force force reindexing even when the index is up to date 2014027a91aSSatoshi Sahara * @return bool If the function completed successfully 2026225b270SMichael Große * 203a32da6ddSSatoshi Sahara * @throws IndexAccessException 204a16bd548SSatoshi Sahara * @throws IndexLockException 205a16bd548SSatoshi Sahara * @throws IndexWriteException 2064027a91aSSatoshi Sahara * @author Satoshi Sahara <sahara.satoshi@gmail.com> 20715f699acSAndreas Gohr * @author Tom N Harris <tnharris@whoopdedo.org> 2086225b270SMichael Große */ 209a32da6ddSSatoshi Sahara public function deletePage($verbose = false, $force = false) 2104027a91aSSatoshi Sahara { 211a32da6ddSSatoshi Sahara if (!isset($this->page)) { 212a32da6ddSSatoshi Sahara throw new IndexAccessException('Indexer: invalid page name in deletePage'); 213a32da6ddSSatoshi Sahara } else { 214a32da6ddSSatoshi Sahara $page = $this->page; 215a32da6ddSSatoshi Sahara } 216a32da6ddSSatoshi Sahara 2174027a91aSSatoshi Sahara $idxtag = metaFN($page,'.indexed'); 2184027a91aSSatoshi Sahara if (!$force && !file_exists($idxtag)) { 2194027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: {$page}.indexed file does not exist, ignoring"); 2204027a91aSSatoshi Sahara return true; 2214027a91aSSatoshi Sahara } 2226225b270SMichael Große 223*725e8e5fSSatoshi Sahara // retrieve pid from the page.idx file, $pid is always integer 224*725e8e5fSSatoshi Sahara $pid = $this->getPID($page); 225*725e8e5fSSatoshi Sahara 226743c9a28SSatoshi Sahara // remove obsoleted content from Fulltext index 227*725e8e5fSSatoshi Sahara $result = (new FulltextIndex($pid))->deleteWords(); 228*725e8e5fSSatoshi Sahara if ($verbose) dbglog("Indexer: deleteWords() for {$page} done"); 2294027a91aSSatoshi Sahara if (!$result) { 2304027a91aSSatoshi Sahara return false; 2314027a91aSSatoshi Sahara } 2326225b270SMichael Große 2334027a91aSSatoshi Sahara // delete all keys of the page from metadata index 234*725e8e5fSSatoshi Sahara $result = (new MetadataIndex($pid))->deleteMetaKeys(); 235*725e8e5fSSatoshi Sahara if ($verbose) dbglog("Indexer: deleteMetaKeys() for {$page} done"); 2364027a91aSSatoshi Sahara if (!$result) { 2374027a91aSSatoshi Sahara return false; 2384027a91aSSatoshi Sahara } 2394027a91aSSatoshi Sahara 2404027a91aSSatoshi Sahara // mark the page as deleted in the page.idx 241a16bd548SSatoshi Sahara $this->lock(); 242a16bd548SSatoshi Sahara $this->saveIndexKey('page', '', $pid, self::INDEX_MARK_DELETED.$page); 243a16bd548SSatoshi Sahara if ($verbose) dbglog("Indexer: {$page} has marked as deleted in page.idx"); 2446225b270SMichael Große $this->unlock(); 2454027a91aSSatoshi Sahara 2464027a91aSSatoshi Sahara unset(static::$pidCache[$pid]); 2474027a91aSSatoshi Sahara @unlink($idxtag); 2484027a91aSSatoshi Sahara return $result; 2494027a91aSSatoshi Sahara } 2504027a91aSSatoshi Sahara 2514027a91aSSatoshi Sahara /** 2524027a91aSSatoshi Sahara * Rename a page in the search index without changing the indexed content. 2534027a91aSSatoshi Sahara * This function doesn't check if the old or new name exists in the filesystem. 2544027a91aSSatoshi Sahara * It returns an error if the old page isn't in the page list of the indexer 2554027a91aSSatoshi Sahara * and it deletes all previously indexed content of the new page. 2564027a91aSSatoshi Sahara * 2574027a91aSSatoshi Sahara * @param string $oldpage The old page name 2584027a91aSSatoshi Sahara * @param string $newpage The new page name 2594027a91aSSatoshi Sahara * @return bool If the page was successfully renamed 260a16bd548SSatoshi Sahara * @throws IndexLockException 261a16bd548SSatoshi Sahara * @throws IndexWriteException 2624027a91aSSatoshi Sahara */ 2634027a91aSSatoshi Sahara public function renamePage($oldpage, $newpage) 2644027a91aSSatoshi Sahara { 2654027a91aSSatoshi Sahara $index = $this->getIndex('page', ''); 2664027a91aSSatoshi Sahara // check if oldpage found in page.idx 2674027a91aSSatoshi Sahara $oldPid = array_search($oldpage, $index, true); 2684027a91aSSatoshi Sahara if ($oldPid === false) return false; 2694027a91aSSatoshi Sahara 2704027a91aSSatoshi Sahara // check if newpage found in page.idx 2714027a91aSSatoshi Sahara $newPid = array_search($newpage, $index, true); 2724027a91aSSatoshi Sahara if ($newPid !== false) { 273a32da6ddSSatoshi Sahara $result = (new Indexer($newpage))->deletePage(); 2744027a91aSSatoshi Sahara if (!$result) return false; 2754027a91aSSatoshi Sahara // Note: $index is no longer valid after deletePage()! 2764027a91aSSatoshi Sahara unset($index); 2774027a91aSSatoshi Sahara } 2784027a91aSSatoshi Sahara 2794027a91aSSatoshi Sahara // update page.idx 280a16bd548SSatoshi Sahara $this->lock(); 281a16bd548SSatoshi Sahara $this->saveIndexKey('page', '', $oldPid, $newpage); 2824027a91aSSatoshi Sahara $this->unlock(); 2834027a91aSSatoshi Sahara 2844027a91aSSatoshi Sahara // reset the pid cache 2854027a91aSSatoshi Sahara $this->resetPIDCache(); 2866225b270SMichael Große 287a16bd548SSatoshi Sahara return true; 2886225b270SMichael Große } 2896225b270SMichael Große 2906225b270SMichael Große /** 2914027a91aSSatoshi Sahara * Clear the Page Index 2926225b270SMichael Große * 293abb227bcSSatoshi Sahara * @param bool $requireLock should be false only if the caller is resposible for index lock 2946225b270SMichael Große * @return bool If the index has been cleared successfully 29515f699acSAndreas Gohr * @throws Exception\IndexLockException 2966225b270SMichael Große */ 2974027a91aSSatoshi Sahara public function clear($requireLock = true) 2984027a91aSSatoshi Sahara { 2996225b270SMichael Große global $conf; 3006225b270SMichael Große 30115f699acSAndreas Gohr if ($requireLock) $this->lock(); 3024027a91aSSatoshi Sahara 3034027a91aSSatoshi Sahara // clear Metadata Index 304a32da6ddSSatoshi Sahara (new MetadataIndex())->clear(false); 3054027a91aSSatoshi Sahara 306743c9a28SSatoshi Sahara // clear Fulltext Index 307a32da6ddSSatoshi Sahara (new FulltextIndex())->clear(false); 3086225b270SMichael Große 3096225b270SMichael Große @unlink($conf['indexdir'].'/page.idx'); 3106225b270SMichael Große 3116225b270SMichael Große // clear the pid cache 3124027a91aSSatoshi Sahara $this->resetPIDCache(); 3136225b270SMichael Große 3144027a91aSSatoshi Sahara if ($requireLock) $this->unlock(); 3156225b270SMichael Große return true; 3166225b270SMichael Große } 3176225b270SMichael Große 3186225b270SMichael Große} 319