16225b270SMichael Große<?php 26225b270SMichael Große 36225b270SMichael Großenamespace dokuwiki\Search; 46225b270SMichael Große 56225b270SMichael Großeuse dokuwiki\Extension\Event; 6743c9a28SSatoshi Saharause dokuwiki\Search\FulltextIndex; 74027a91aSSatoshi Saharause dokuwiki\Search\MetadataIndex; 84027a91aSSatoshi Sahara 94027a91aSSatoshi Sahara// Version tag used to force rebuild on upgrade 104027a91aSSatoshi Saharaconst INDEXER_VERSION = 8; 116225b270SMichael Große 126225b270SMichael Große/** 134027a91aSSatoshi Sahara * Class DokuWiki Indexer (Singleton) 146225b270SMichael Große * 154027a91aSSatoshi Sahara * @license GPL 2 (http://www.gnu.org/licenses/gpl.html) 166225b270SMichael Große * @author Andreas Gohr <andi@splitbrain.org> 174027a91aSSatoshi Sahara * @author Tom N Harris <tnharris@whoopdedo.org> 186225b270SMichael Große */ 194027a91aSSatoshi Saharaclass Indexer extends AbstractIndex 204027a91aSSatoshi Sahara{ 214027a91aSSatoshi Sahara /** @var Indexer $instance */ 224027a91aSSatoshi Sahara protected static $instance = null; 236225b270SMichael Große 244027a91aSSatoshi Sahara /** 254027a91aSSatoshi Sahara * Get new or existing singleton instance of the Indexer 264027a91aSSatoshi Sahara * 274027a91aSSatoshi Sahara * @return Indexer 284027a91aSSatoshi Sahara */ 294027a91aSSatoshi Sahara public static function getInstance() 304027a91aSSatoshi Sahara { 314027a91aSSatoshi Sahara if (is_null(static::$instance)) { 324027a91aSSatoshi Sahara static::$instance = new static(); 336225b270SMichael Große } 344027a91aSSatoshi Sahara return static::$instance; 356225b270SMichael Große } 366225b270SMichael Große 376225b270SMichael Große /** 384027a91aSSatoshi Sahara * Dispatch Indexing request for the page, called by TaskRunner::runIndexer() 396225b270SMichael Große * 404027a91aSSatoshi Sahara * @param string $page name of the page to index 414027a91aSSatoshi Sahara * @param bool $verbose print status messages 424027a91aSSatoshi Sahara * @param bool $force force reindexing even when the index is up to date 434027a91aSSatoshi Sahara * @return bool If the function completed successfully 446225b270SMichael Große * 456225b270SMichael Große * @author Tom N Harris <tnharris@whoopdedo.org> 464027a91aSSatoshi Sahara * @author Satoshi Sahara <sahara.satoshi@gmail.com> 476225b270SMichael Große */ 484027a91aSSatoshi Sahara public function dispatch($page, $verbose = false, $force = false) 494027a91aSSatoshi Sahara { 504027a91aSSatoshi Sahara // check if page was deleted but is still in the index 514027a91aSSatoshi Sahara if (!page_exists($page)) { 5211d2e7d0SSatoshi Sahara return $this->deletePage($page, $verbose, $force); 536225b270SMichael Große } 5411d2e7d0SSatoshi Sahara 5511d2e7d0SSatoshi Sahara // update search index 5611d2e7d0SSatoshi Sahara return $this->addPage($page, $verbose, $force); 576225b270SMichael Große } 586225b270SMichael Große 596225b270SMichael Große /** 604027a91aSSatoshi Sahara * Version of the indexer taking into consideration the external tokenizer. 614027a91aSSatoshi Sahara * The indexer is only compatible with data written by the same version. 626225b270SMichael Große * 634027a91aSSatoshi Sahara * @triggers INDEXER_VERSION_GET 644027a91aSSatoshi Sahara * Plugins that modify what gets indexed should hook this event and 654027a91aSSatoshi Sahara * add their version info to the event data like so: 664027a91aSSatoshi Sahara * $data[$plugin_name] = $plugin_version; 676225b270SMichael Große * 686225b270SMichael Große * @author Tom N Harris <tnharris@whoopdedo.org> 696225b270SMichael Große * @author Michael Hamann <michael@content-space.de> 704027a91aSSatoshi Sahara * 714027a91aSSatoshi Sahara * @return int|string 726225b270SMichael Große */ 734027a91aSSatoshi Sahara public function getVersion() 744027a91aSSatoshi Sahara { 754027a91aSSatoshi Sahara static $indexer_version = null; 764027a91aSSatoshi Sahara if ($indexer_version == null) { 774027a91aSSatoshi Sahara $version = INDEXER_VERSION; 784027a91aSSatoshi Sahara 794027a91aSSatoshi Sahara // DokuWiki version is included for the convenience of plugins 804027a91aSSatoshi Sahara $data = array('dokuwiki' => $version); 814027a91aSSatoshi Sahara Event::createAndTrigger('INDEXER_VERSION_GET', $data, null, false); 824027a91aSSatoshi Sahara unset($data['dokuwiki']); // this needs to be first 834027a91aSSatoshi Sahara ksort($data); 844027a91aSSatoshi Sahara foreach ($data as $plugin => $vers) { 854027a91aSSatoshi Sahara $version .= '+'.$plugin.'='.$vers; 864027a91aSSatoshi Sahara } 874027a91aSSatoshi Sahara $indexer_version = $version; 884027a91aSSatoshi Sahara } 894027a91aSSatoshi Sahara return $indexer_version; 906225b270SMichael Große } 916225b270SMichael Große 924027a91aSSatoshi Sahara /** 934027a91aSSatoshi Sahara * Adds/updates the search index for the given page 944027a91aSSatoshi Sahara * 954027a91aSSatoshi Sahara * Locking is handled internally. 964027a91aSSatoshi Sahara * 974027a91aSSatoshi Sahara * @param string $page name of the page to index 984027a91aSSatoshi Sahara * @param bool $verbose print status messages 994027a91aSSatoshi Sahara * @param bool $force force reindexing even when the index is up to date 1004027a91aSSatoshi Sahara * @return bool If the function completed successfully 1014027a91aSSatoshi Sahara * 1024027a91aSSatoshi Sahara * @author Tom N Harris <tnharris@whoopdedo.org> 1034027a91aSSatoshi Sahara * @author Satoshi Sahara <sahara.satoshi@gmail.com> 1044027a91aSSatoshi Sahara */ 1054027a91aSSatoshi Sahara public function addPage($page, $verbose = false, $force = false) 1064027a91aSSatoshi Sahara { 1074027a91aSSatoshi Sahara // check if indexing needed for the existing page (full text and/or metadata indexing) 1084027a91aSSatoshi Sahara $idxtag = metaFN($page,'.indexed'); 1094027a91aSSatoshi Sahara if (!$force && file_exists($idxtag)) { 1104027a91aSSatoshi Sahara if (trim(io_readFile($idxtag)) == $this->getVersion()) { 1114027a91aSSatoshi Sahara $last = @filemtime($idxtag); 1124027a91aSSatoshi Sahara if ($last > @filemtime(wikiFN($page))) { 1134027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: index for {$page} up to date"); 1144027a91aSSatoshi Sahara return true; 1154027a91aSSatoshi Sahara } 1164027a91aSSatoshi Sahara } 1174027a91aSSatoshi Sahara } 1186225b270SMichael Große 1194027a91aSSatoshi Sahara // register the page to the page.idx 1204027a91aSSatoshi Sahara $pid = $this->getPID($page); 1216225b270SMichael Große if ($pid === false) { 1224027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: getting the PID failed for {$page}"); 1234027a91aSSatoshi Sahara trigger_error("Failed to get PID for {$page}", E_USER_ERROR); 1246225b270SMichael Große return false; 1256225b270SMichael Große } 1266225b270SMichael Große 1274027a91aSSatoshi Sahara // prepare metadata indexing 1284027a91aSSatoshi Sahara $metadata = array(); 1294027a91aSSatoshi Sahara $metadata['title'] = p_get_metadata($page, 'title', METADATA_RENDER_UNLIMITED); 1306225b270SMichael Große 1314027a91aSSatoshi Sahara $references = p_get_metadata($page, 'relation references', METADATA_RENDER_UNLIMITED); 1324027a91aSSatoshi Sahara $metadata['relation_references'] = ($references !== null) ? 1334027a91aSSatoshi Sahara array_keys($references) : array(); 1346225b270SMichael Große 1354027a91aSSatoshi Sahara $media = p_get_metadata($page, 'relation media', METADATA_RENDER_UNLIMITED); 1364027a91aSSatoshi Sahara $metadata['relation_media'] = ($media !== null) ? 1374027a91aSSatoshi Sahara array_keys($media) : array(); 1386225b270SMichael Große 1394027a91aSSatoshi Sahara // check if full text indexing allowed 1404027a91aSSatoshi Sahara $indexenabled = p_get_metadata($page, 'internal index', METADATA_RENDER_UNLIMITED); 1414027a91aSSatoshi Sahara if ($indexenabled !== false) $indexenabled = true; 1424027a91aSSatoshi Sahara $metadata['internal_index'] = $indexenabled; 1436225b270SMichael Große 1444027a91aSSatoshi Sahara $body = ''; 1454027a91aSSatoshi Sahara $data = compact('page', 'body', 'metadata', 'pid'); 1464027a91aSSatoshi Sahara $event = new Event('INDEXER_PAGE_ADD', $data); 1474027a91aSSatoshi Sahara if ($event->advise_before()) $data['body'] = $data['body'].' '.rawWiki($page); 1484027a91aSSatoshi Sahara $event->advise_after(); 1494027a91aSSatoshi Sahara unset($event); 1504027a91aSSatoshi Sahara extract($data); 1514027a91aSSatoshi Sahara $indexenabled = $metadata['internal_index']; 1524027a91aSSatoshi Sahara unset($metadata['internal_index']); 1536225b270SMichael Große 1544027a91aSSatoshi Sahara // Access to Metadata Index 1554027a91aSSatoshi Sahara $MetadataIndex = MetadataIndex::getInstance(); 1564027a91aSSatoshi Sahara $result = $MetadataIndex->addMetaKeys($page, $metadata); 1574027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: addMetaKeys({$page}) ".($result ? 'done' : 'failed')); 1584027a91aSSatoshi Sahara if (!$result) { 1596225b270SMichael Große return false; 1606225b270SMichael Große } 1616225b270SMichael Große 162743c9a28SSatoshi Sahara // Access to Fulltext Index 163743c9a28SSatoshi Sahara $FulltextIndex = FulltextIndex::getInstance(); 1644027a91aSSatoshi Sahara if ($indexenabled) { 165743c9a28SSatoshi Sahara $result = $FulltextIndex->addPagewords($page, $body); 1664027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: addPageWords({$page}) ".($result ? 'done' : 'failed')); 1674027a91aSSatoshi Sahara if (!$result) { 1686225b270SMichael Große return false; 1696225b270SMichael Große } 1706225b270SMichael Große } else { 1714027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: full text indexing disabled for {$page}"); 172743c9a28SSatoshi Sahara // ensure the page content deleted from the Fulltext index 173743c9a28SSatoshi Sahara $result = $FulltextIndex->deletePageWords($page); 1744027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: deletePageWords({$page}) ".($result ? 'done' : 'failed')); 1754027a91aSSatoshi Sahara if (!$result) { 1766225b270SMichael Große return false; 1776225b270SMichael Große } 1786225b270SMichael Große } 1796225b270SMichael Große 1804027a91aSSatoshi Sahara // update index tag file 1814027a91aSSatoshi Sahara io_saveFile($idxtag, $this->getVersion()); 1824027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: finished"); 1834027a91aSSatoshi Sahara 1844027a91aSSatoshi Sahara return $result; 1856225b270SMichael Große } 1866225b270SMichael Große 1876225b270SMichael Große /** 188*5f9bd525SSatoshi Sahara * Remove a page from the index 1896225b270SMichael Große * 190*5f9bd525SSatoshi Sahara * Erases entries in all known indexes. Locking is handled internally. 1916225b270SMichael Große * 1924027a91aSSatoshi Sahara * @param string $page name of the page to index 1934027a91aSSatoshi Sahara * @param bool $verbose print status messages 1944027a91aSSatoshi Sahara * @param bool $force force reindexing even when the index is up to date 1954027a91aSSatoshi Sahara * @return bool If the function completed successfully 1966225b270SMichael Große * 1976225b270SMichael Große * @author Tom N Harris <tnharris@whoopdedo.org> 1984027a91aSSatoshi Sahara * @author Satoshi Sahara <sahara.satoshi@gmail.com> 1996225b270SMichael Große */ 2004027a91aSSatoshi Sahara public function deletePage($page, $verbose = false, $force = false) 2014027a91aSSatoshi Sahara { 2024027a91aSSatoshi Sahara $idxtag = metaFN($page,'.indexed'); 2034027a91aSSatoshi Sahara if (!$force && !file_exists($idxtag)) { 2044027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: {$page}.indexed file does not exist, ignoring"); 2054027a91aSSatoshi Sahara return true; 2064027a91aSSatoshi Sahara } 2076225b270SMichael Große 208743c9a28SSatoshi Sahara // remove obsoleted content from Fulltext index 209743c9a28SSatoshi Sahara $FulltextIndex = FulltextIndex::getInstance(); 210743c9a28SSatoshi Sahara $result = $FulltextIndex->deletePageWords($page); 2114027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: deletePageWords({$page}) ".($result ? 'done' : 'failed')); 2124027a91aSSatoshi Sahara if (!$result) { 2134027a91aSSatoshi Sahara return false; 2144027a91aSSatoshi Sahara } 2156225b270SMichael Große 2164027a91aSSatoshi Sahara // delete all keys of the page from metadata index 2174027a91aSSatoshi Sahara $MetadataIndex = MetadataIndex::getInstance(); 2184027a91aSSatoshi Sahara $result = $MetadataIndex->deleteMetaKeys($page); 2194027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: deleteMetaKeys({$page}) ".($result ? 'done' : 'failed')); 2204027a91aSSatoshi Sahara if (!$result) { 2214027a91aSSatoshi Sahara return false; 2224027a91aSSatoshi Sahara } 2234027a91aSSatoshi Sahara 2244027a91aSSatoshi Sahara // mark the page as deleted in the page.idx 2254027a91aSSatoshi Sahara $pid = $this->getPID($page); 2264027a91aSSatoshi Sahara if ($pid !== false) { 2275237d405SSatoshi Sahara if (!$this->lock()) return false; 228653b91a2SSatoshi Sahara $result = $this->saveIndexKey('page', '', $pid, self::INDEX_MARK_DELETED.$page); 2294027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: update page.idx ".($result ? 'done' : 'failed')); 2306225b270SMichael Große $this->unlock(); 2314027a91aSSatoshi Sahara } else { 2324027a91aSSatoshi Sahara if ($verbose) dbglog("Indexer: {$page} not found in the page.idx, ignoring"); 233a2f39162SSatoshi Sahara $result = true; 2344027a91aSSatoshi Sahara } 2354027a91aSSatoshi Sahara 2364027a91aSSatoshi Sahara unset(static::$pidCache[$pid]); 2374027a91aSSatoshi Sahara @unlink($idxtag); 2384027a91aSSatoshi Sahara return $result; 2394027a91aSSatoshi Sahara } 2404027a91aSSatoshi Sahara 2414027a91aSSatoshi Sahara /** 2424027a91aSSatoshi Sahara * Rename a page in the search index without changing the indexed content. 2434027a91aSSatoshi Sahara * This function doesn't check if the old or new name exists in the filesystem. 2444027a91aSSatoshi Sahara * It returns an error if the old page isn't in the page list of the indexer 2454027a91aSSatoshi Sahara * and it deletes all previously indexed content of the new page. 2464027a91aSSatoshi Sahara * 2474027a91aSSatoshi Sahara * @param string $oldpage The old page name 2484027a91aSSatoshi Sahara * @param string $newpage The new page name 2494027a91aSSatoshi Sahara * @return bool If the page was successfully renamed 2504027a91aSSatoshi Sahara */ 2514027a91aSSatoshi Sahara public function renamePage($oldpage, $newpage) 2524027a91aSSatoshi Sahara { 2534027a91aSSatoshi Sahara $index = $this->getIndex('page', ''); 2544027a91aSSatoshi Sahara // check if oldpage found in page.idx 2554027a91aSSatoshi Sahara $oldPid = array_search($oldpage, $index, true); 2564027a91aSSatoshi Sahara if ($oldPid === false) return false; 2574027a91aSSatoshi Sahara 2584027a91aSSatoshi Sahara // check if newpage found in page.idx 2594027a91aSSatoshi Sahara $newPid = array_search($newpage, $index, true); 2604027a91aSSatoshi Sahara if ($newPid !== false) { 2614027a91aSSatoshi Sahara $result = $this->deletePage($newpage); 2624027a91aSSatoshi Sahara if (!$result) return false; 2634027a91aSSatoshi Sahara // Note: $index is no longer valid after deletePage()! 2644027a91aSSatoshi Sahara unset($index); 2654027a91aSSatoshi Sahara } 2664027a91aSSatoshi Sahara 2674027a91aSSatoshi Sahara // update page.idx 2685237d405SSatoshi Sahara if (!$this->lock()) return false; 2694027a91aSSatoshi Sahara $result = $this->saveIndexKey('page', '', $oldPid, $newpage); 2704027a91aSSatoshi Sahara $this->unlock(); 2714027a91aSSatoshi Sahara 2724027a91aSSatoshi Sahara // reset the pid cache 2734027a91aSSatoshi Sahara $this->resetPIDCache(); 2746225b270SMichael Große 2756225b270SMichael Große return $result; 2766225b270SMichael Große } 2776225b270SMichael Große 2786225b270SMichael Große /** 2794027a91aSSatoshi Sahara * Clear the Page Index 2806225b270SMichael Große * 2814027a91aSSatoshi Sahara * @param bool $requireLock 2826225b270SMichael Große * @return bool If the index has been cleared successfully 2836225b270SMichael Große */ 2844027a91aSSatoshi Sahara public function clear($requireLock = true) 2854027a91aSSatoshi Sahara { 2866225b270SMichael Große global $conf; 2876225b270SMichael Große 2884027a91aSSatoshi Sahara if ($requireLock && !$this->lock()) return false; 2894027a91aSSatoshi Sahara 2904027a91aSSatoshi Sahara // clear Metadata Index 2914027a91aSSatoshi Sahara $MetadataIndex = MetadataIndex::getInstance(); 2924027a91aSSatoshi Sahara $MetadataIndex->clear(false); 2934027a91aSSatoshi Sahara 294743c9a28SSatoshi Sahara // clear Fulltext Index 295743c9a28SSatoshi Sahara $FulltextIndex = FulltextIndex::getInstance(); 296743c9a28SSatoshi Sahara $FulltextIndex->clear(false); 2976225b270SMichael Große 2986225b270SMichael Große @unlink($conf['indexdir'].'/page.idx'); 2996225b270SMichael Große 3006225b270SMichael Große // clear the pid cache 3014027a91aSSatoshi Sahara $this->resetPIDCache(); 3026225b270SMichael Große 3034027a91aSSatoshi Sahara if ($requireLock) $this->unlock(); 3046225b270SMichael Große return true; 3056225b270SMichael Große } 3066225b270SMichael Große 3076225b270SMichael Große 3086225b270SMichael Große /** 3096225b270SMichael Große * Return a list of words sorted by number of times used 3106225b270SMichael Große * 3116225b270SMichael Große * @param int $min bottom frequency threshold 3126225b270SMichael Große * @param int $max upper frequency limit. No limit if $max<$min 3136225b270SMichael Große * @param int $minlen minimum length of words to count 3146225b270SMichael Große * @param string $key metadata key to list. Uses the fulltext index if not given 3156225b270SMichael Große * @return array list of words as the keys and frequency as values 3166225b270SMichael Große * 3176225b270SMichael Große * @author Tom N Harris <tnharris@whoopdedo.org> 3186225b270SMichael Große */ 319*5f9bd525SSatoshi Sahara public function histogram($min=1, $max=0, $minlen=3, $key=null) 3204027a91aSSatoshi Sahara { 3214027a91aSSatoshi Sahara if ($min < 1) $min = 1; 3224027a91aSSatoshi Sahara if ($max < $min) $max = 0; 3236225b270SMichael Große 3246225b270SMichael Große $result = array(); 3256225b270SMichael Große 3266225b270SMichael Große if ($key == 'title') { 3276225b270SMichael Große $index = $this->getIndex('title', ''); 3286225b270SMichael Große $index = array_count_values($index); 3296225b270SMichael Große foreach ($index as $val => $cnt) { 3304027a91aSSatoshi Sahara if ($cnt >= $min && (!$max || $cnt <= $max) && strlen($val) >= $minlen) { 3316225b270SMichael Große $result[$val] = $cnt; 3326225b270SMichael Große } 3336225b270SMichael Große } 3344027a91aSSatoshi Sahara } elseif (!is_null($key)) { 3354027a91aSSatoshi Sahara $metaname = $this->cleanName($key); 3366225b270SMichael Große $index = $this->getIndex($metaname.'_i', ''); 3376225b270SMichael Große $val_idx = array(); 3386225b270SMichael Große foreach ($index as $wid => $line) { 3396225b270SMichael Große $freq = $this->countTuples($line); 3404027a91aSSatoshi Sahara if ($freq >= $min && (!$max || $freq <= $max)) { 3416225b270SMichael Große $val_idx[$wid] = $freq; 3426225b270SMichael Große } 3434027a91aSSatoshi Sahara } 3446225b270SMichael Große if (!empty($val_idx)) { 3456225b270SMichael Große $words = $this->getIndex($metaname.'_w', ''); 3466225b270SMichael Große foreach ($val_idx as $wid => $freq) { 3474027a91aSSatoshi Sahara if (strlen($words[$wid]) >= $minlen) { 3486225b270SMichael Große $result[$words[$wid]] = $freq; 3496225b270SMichael Große } 3506225b270SMichael Große } 3516225b270SMichael Große } 3524027a91aSSatoshi Sahara } else { 353743c9a28SSatoshi Sahara $FulltextIndex = FulltextIndex::getInstance(); 354743c9a28SSatoshi Sahara $lengths = $FulltextIndex->listIndexLengths(); 3556225b270SMichael Große foreach ($lengths as $length) { 3566225b270SMichael Große if ($length < $minlen) continue; 3576225b270SMichael Große $index = $this->getIndex('i', $length); 3586225b270SMichael Große $words = null; 3596225b270SMichael Große foreach ($index as $wid => $line) { 3606225b270SMichael Große $freq = $this->countTuples($line); 3616225b270SMichael Große if ($freq >= $min && (!$max || $freq <= $max)) { 3624027a91aSSatoshi Sahara if ($words === null) { 3636225b270SMichael Große $words = $this->getIndex('w', $length); 3644027a91aSSatoshi Sahara } 3656225b270SMichael Große $result[$words[$wid]] = $freq; 3666225b270SMichael Große } 3676225b270SMichael Große } 3686225b270SMichael Große } 3696225b270SMichael Große } 3706225b270SMichael Große 3716225b270SMichael Große arsort($result); 3726225b270SMichael Große return $result; 3736225b270SMichael Große } 3746225b270SMichael Große} 375