xref: /dokuwiki/inc/Search/Indexer.php (revision 11d2e7d0b6c01d01c6214ba4615f13d70e0da4b2)
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)) {
52*11d2e7d0SSatoshi Sahara            return $this->deletePage($page, $verbose, $force);
536225b270SMichael Große        }
54*11d2e7d0SSatoshi Sahara
55*11d2e7d0SSatoshi Sahara        // update search index
56*11d2e7d0SSatoshi 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    /**
1884d04b7bbSSatoshi Sahara     * Remove a page from the index, erases entries in all known indexes
1896225b270SMichael Große     *
1904d04b7bbSSatoshi Sahara     * 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     */
3194d04b7bbSSatoshi Sahara    public function histogram($key = null, $min = 1, $max = 0, $minlen = 3)
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