xref: /dokuwiki/inc/Search/Indexer.php (revision a32da6dda625a55bd713657b22f3ab332757fca6)
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/**
14*a32da6ddSSatoshi 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{
22*a32da6ddSSatoshi Sahara    // page to be indexed
23*a32da6ddSSatoshi Sahara    protected $page;
246225b270SMichael Große
254027a91aSSatoshi Sahara    /**
26*a32da6ddSSatoshi Sahara     * Indexer constructor
274027a91aSSatoshi Sahara     *
28*a32da6ddSSatoshi Sahara     * @param string $page name of the page to index
294027a91aSSatoshi Sahara     * @return Indexer
304027a91aSSatoshi Sahara     */
31*a32da6ddSSatoshi Sahara    public function __construct($page = null)
324027a91aSSatoshi Sahara    {
33*a32da6ddSSatoshi 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     *
43*a32da6ddSSatoshi 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     */
49*a32da6ddSSatoshi Sahara    public function dispatch($verbose = false, $force = false)
504027a91aSSatoshi Sahara    {
51*a32da6ddSSatoshi Sahara        if (!isset($this->page)) {
52*a32da6ddSSatoshi Sahara            throw new IndexAccessException('Indexer: unknow page name');
53*a32da6ddSSatoshi Sahara        }
54*a32da6ddSSatoshi Sahara
554027a91aSSatoshi Sahara        // check if page was deleted but is still in the index
56*a32da6ddSSatoshi Sahara        if (!page_exists($this->page)) {
57*a32da6ddSSatoshi Sahara            return $this->deletePage($verbose, $force);
586225b270SMichael Große        }
5911d2e7d0SSatoshi Sahara
6011d2e7d0SSatoshi Sahara        // update search index
61*a32da6ddSSatoshi 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     *
106*a32da6ddSSatoshi 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     */
112*a32da6ddSSatoshi Sahara    public function addPage($verbose = false, $force = false)
1134027a91aSSatoshi Sahara    {
114*a32da6ddSSatoshi Sahara        if (!isset($this->page)) {
115*a32da6ddSSatoshi Sahara            throw new IndexAccessException('Indexer: invalid page name in addePage');
116*a32da6ddSSatoshi Sahara        } else {
117*a32da6ddSSatoshi Sahara            $page = $this->page;
118*a32da6ddSSatoshi Sahara        }
119*a32da6ddSSatoshi 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
132a16bd548SSatoshi Sahara        // register the page to the page.idx file, $pid is always numeric
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*a32da6ddSSatoshi Sahara        $result = (new MetadataIndex($page))->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*a32da6ddSSatoshi Sahara            $result = (new FulltextIndex($page))->addWords($body);
172*a32da6ddSSatoshi Sahara            if ($verbose) dbglog("Indexer: addPageWords({$page}) ".($result ? 'done' : 'failed')); // FIXME
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
179*a32da6ddSSatoshi Sahara            $result = (new FulltextIndex($page))->deleteWords();
180*a32da6ddSSatoshi Sahara            if ($verbose) dbglog("Indexer: deletePageWords({$page}) ".($result ? 'done' : 'failed')); // FIXME
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     *
203*a32da6ddSSatoshi 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     */
209*a32da6ddSSatoshi Sahara    public function deletePage($verbose = false, $force = false)
2104027a91aSSatoshi Sahara    {
211*a32da6ddSSatoshi Sahara        if (!isset($this->page)) {
212*a32da6ddSSatoshi Sahara            throw new IndexAccessException('Indexer: invalid page name in deletePage');
213*a32da6ddSSatoshi Sahara        } else {
214*a32da6ddSSatoshi Sahara            $page = $this->page;
215*a32da6ddSSatoshi Sahara        }
216*a32da6ddSSatoshi 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
223743c9a28SSatoshi Sahara        // remove obsoleted content from Fulltext index
224*a32da6ddSSatoshi Sahara        $result = (new FulltextIndex($page))->deleteWords();
225*a32da6ddSSatoshi Sahara        if ($verbose) dbglog("Indexer: deletePageWords({$page}) ".($result ? 'done' : 'failed')); // FIXME
2264027a91aSSatoshi Sahara        if (!$result) {
2274027a91aSSatoshi Sahara            return false;
2284027a91aSSatoshi Sahara        }
2296225b270SMichael Große
2304027a91aSSatoshi Sahara        // delete all keys of the page from metadata index
231*a32da6ddSSatoshi Sahara        $result = (new MetadataIndex($page))->deleteMetaKeys();
232*a32da6ddSSatoshi Sahara        if ($verbose) dbglog("Indexer: deleteMetaKeys({$page}) ".($result ? 'done' : 'failed')); // FIXME
2334027a91aSSatoshi Sahara        if (!$result) {
2344027a91aSSatoshi Sahara            return false;
2354027a91aSSatoshi Sahara        }
2364027a91aSSatoshi Sahara
2374027a91aSSatoshi Sahara        // mark the page as deleted in the page.idx
2384027a91aSSatoshi Sahara        $pid = $this->getPID($page);
239a16bd548SSatoshi Sahara        $this->lock();
240a16bd548SSatoshi Sahara        $this->saveIndexKey('page', '', $pid, self::INDEX_MARK_DELETED.$page);
241a16bd548SSatoshi Sahara        if ($verbose) dbglog("Indexer: {$page} has marked as deleted in page.idx");
2426225b270SMichael Große        $this->unlock();
2434027a91aSSatoshi Sahara
2444027a91aSSatoshi Sahara        unset(static::$pidCache[$pid]);
2454027a91aSSatoshi Sahara        @unlink($idxtag);
2464027a91aSSatoshi Sahara        return $result;
2474027a91aSSatoshi Sahara    }
2484027a91aSSatoshi Sahara
2494027a91aSSatoshi Sahara    /**
2504027a91aSSatoshi Sahara     * Rename a page in the search index without changing the indexed content.
2514027a91aSSatoshi Sahara     * This function doesn't check if the old or new name exists in the filesystem.
2524027a91aSSatoshi Sahara     * It returns an error if the old page isn't in the page list of the indexer
2534027a91aSSatoshi Sahara     * and it deletes all previously indexed content of the new page.
2544027a91aSSatoshi Sahara     *
2554027a91aSSatoshi Sahara     * @param string $oldpage The old page name
2564027a91aSSatoshi Sahara     * @param string $newpage The new page name
2574027a91aSSatoshi Sahara     * @return bool  If the page was successfully renamed
258a16bd548SSatoshi Sahara     * @throws IndexLockException
259a16bd548SSatoshi Sahara     * @throws IndexWriteException
2604027a91aSSatoshi Sahara     */
2614027a91aSSatoshi Sahara    public function renamePage($oldpage, $newpage)
2624027a91aSSatoshi Sahara    {
2634027a91aSSatoshi Sahara        $index = $this->getIndex('page', '');
2644027a91aSSatoshi Sahara        // check if oldpage found in page.idx
2654027a91aSSatoshi Sahara        $oldPid = array_search($oldpage, $index, true);
2664027a91aSSatoshi Sahara        if ($oldPid === false) return false;
2674027a91aSSatoshi Sahara
2684027a91aSSatoshi Sahara        // check if newpage found in page.idx
2694027a91aSSatoshi Sahara        $newPid = array_search($newpage, $index, true);
2704027a91aSSatoshi Sahara        if ($newPid !== false) {
271*a32da6ddSSatoshi Sahara            $result = (new Indexer($newpage))->deletePage();
2724027a91aSSatoshi Sahara            if (!$result) return false;
2734027a91aSSatoshi Sahara            // Note: $index is no longer valid after deletePage()!
2744027a91aSSatoshi Sahara            unset($index);
2754027a91aSSatoshi Sahara        }
2764027a91aSSatoshi Sahara
2774027a91aSSatoshi Sahara        // update page.idx
278a16bd548SSatoshi Sahara        $this->lock();
279a16bd548SSatoshi Sahara        $this->saveIndexKey('page', '', $oldPid, $newpage);
2804027a91aSSatoshi Sahara        $this->unlock();
2814027a91aSSatoshi Sahara
2824027a91aSSatoshi Sahara        // reset the pid cache
2834027a91aSSatoshi Sahara        $this->resetPIDCache();
2846225b270SMichael Große
285a16bd548SSatoshi Sahara        return true;
2866225b270SMichael Große    }
2876225b270SMichael Große
2886225b270SMichael Große    /**
2894027a91aSSatoshi Sahara     * Clear the Page Index
2906225b270SMichael Große     *
291abb227bcSSatoshi Sahara     * @param bool $requireLock should be false only if the caller is resposible for index lock
2926225b270SMichael Große     * @return bool  If the index has been cleared successfully
29315f699acSAndreas Gohr     * @throws Exception\IndexLockException
2946225b270SMichael Große     */
2954027a91aSSatoshi Sahara    public function clear($requireLock = true)
2964027a91aSSatoshi Sahara    {
2976225b270SMichael Große        global $conf;
2986225b270SMichael Große
29915f699acSAndreas Gohr        if ($requireLock) $this->lock();
3004027a91aSSatoshi Sahara
3014027a91aSSatoshi Sahara        // clear Metadata Index
302*a32da6ddSSatoshi Sahara        (new MetadataIndex())->clear(false);
3034027a91aSSatoshi Sahara
304743c9a28SSatoshi Sahara        // clear Fulltext Index
305*a32da6ddSSatoshi Sahara        (new FulltextIndex())->clear(false);
3066225b270SMichael Große
3076225b270SMichael Große        @unlink($conf['indexdir'].'/page.idx');
3086225b270SMichael Große
3096225b270SMichael Große        // clear the pid cache
3104027a91aSSatoshi Sahara        $this->resetPIDCache();
3116225b270SMichael Große
3124027a91aSSatoshi Sahara        if ($requireLock) $this->unlock();
3136225b270SMichael Große        return true;
3146225b270SMichael Große    }
3156225b270SMichael Große
3166225b270SMichael Große}
317