123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350 |
- <?php
- namespace Grav\Plugin\TNTSearch;
- use Grav\Common\Config\Config;
- use Grav\Common\Grav;
- use Grav\Common\Language\Language;
- use Grav\Common\Page\Interfaces\PageInterface;
- use Grav\Common\Page\Pages;
- use Grav\Common\Twig\Twig;
- use Grav\Common\Uri;
- use Grav\Common\Yaml;
- use Grav\Common\Page\Collection;
- use Grav\Common\Page\Page;
- use RocketTheme\Toolbox\Event\Event;
- use RocketTheme\Toolbox\ResourceLocator\UniformResourceLocator;
- use TeamTNT\TNTSearch\Exceptions\IndexNotFoundException;
- use TeamTNT\TNTSearch\TNTSearch;
- class GravTNTSearch
- {
- /** @var TNTSearch */
- public $tnt;
- /** @var array */
- protected $options;
- /** @var string[] */
- protected $bool_characters = ['-', '(', ')', 'or'];
- /** @var string */
- protected $index = 'grav.index';
- /** @var false|string */
- protected $language;
- /**
- * GravTNTSearch constructor.
- * @param array $options
- */
- public function __construct($options = [])
- {
- /** @var Config $config */
- $config = Grav::instance()['config'];
- /** @var UniformResourceLocator $locator */
- $locator = Grav::instance()['locator'];
- $search_type = $config->get('plugins.tntsearch.search_type', 'auto');
- $fuzzy = $config->get('plugins.tntsearch.fuzzy', false);
- $distance = $config->get('plugins.tntsearch.distance', 2);
- $stemmer = $config->get('plugins.tntsearch.stemmer', 'no');
- $limit = $config->get('plugins.tntsearch.limit', 20);
- $snippet = $config->get('plugins.tntsearch.snippet', 300);
- $data_path = $locator->findResource('user://data', true) . '/tntsearch';
- /** @var Language $language */
- $language = Grav::instance()['language'];
- if ($language->enabled()) {
- $active = $language->getActive();
- $default = $language->getDefault();
- $this->language = $active ?: $default;
- $this->index = $this->language . '.index';
- }
- if (!file_exists($data_path)) {
- mkdir($data_path);
- }
- $defaults = [
- 'json' => false,
- 'search_type' => $search_type,
- 'fuzzy' => $fuzzy,
- 'distance' => $distance,
- 'stemmer' => $stemmer,
- 'limit' => $limit,
- 'as_you_type' => true,
- 'snippet' => $snippet,
- 'phrases' => true,
- ];
- $this->options = array_replace($defaults, $options);
- $this->tnt = new TNTSearch();
- $this->tnt->loadConfig(
- [
- 'storage' => $data_path,
- 'driver' => 'sqlite',
- 'charset' => 'utf8'
- ]
- );
- }
- /**
- * @param string $query
- * @return object|string
- * @throws IndexNotFoundException
- */
- public function search($query)
- {
- /** @var Uri $uri */
- $uri = Grav::instance()['uri'];
- $type = $uri->query('search_type');
- $this->tnt->selectIndex($this->index);
- $this->tnt->asYouType = $this->options['as_you_type'];
- if (isset($this->options['fuzzy']) && $this->options['fuzzy']) {
- $this->tnt->fuzziness = true;
- $this->tnt->fuzzy_distance = $this->options['distance'];
- }
- $limit = (int)$this->options['limit'];
- $type = $type ?? $this->options['search_type'];
- // TODO: Multiword parameter has been removed from $tnt->search(), please check if this works
- $multiword = null;
- if (isset($this->options['phrases']) && $this->options['phrases']) {
- if (strlen($query) > 2) {
- if ($query[0] === '"' && $query[strlen($query) - 1] === '"') {
- $multiword = substr($query, 1, -1);
- $type = 'basic';
- $query = $multiword;
- }
- }
- }
- switch ($type) {
- case 'basic':
- $results = $this->tnt->search($query, $limit);
- break;
- case 'boolean':
- $results = $this->tnt->searchBoolean($query, $limit);
- break;
- case 'default':
- case 'auto':
- default:
- $guess = 'search';
- foreach ($this->bool_characters as $char) {
- if (strpos($query, $char) !== false) {
- $guess = 'searchBoolean';
- break;
- }
- }
- $results = $this->tnt->{$guess}($query, $limit);
- }
- return $this->processResults($results, $query);
- }
- /**
- * @param array $res
- * @param string $query
- * @return object|string
- */
- protected function processResults($res, $query)
- {
- $data = new \stdClass();
- $data->number_of_hits = $res['hits'] ?? 0;
- $data->execution_time = $res['execution_time'];
- /** @var Pages $pages */
- $pages = Grav::instance()['pages'];
- $counter = 0;
- foreach ($res['ids'] as $path) {
- if ($counter++ > $this->options['limit']) {
- break;
- }
- $page = $pages->find($path);
- if ($page) {
- $event = new Event(
- [
- 'page' => $page,
- 'query' => $query,
- 'options' => $this->options,
- 'fields' => $data,
- 'gtnt' => $this
- ]
- );
- Grav::instance()->fireEvent('onTNTSearchQuery', $event);
- }
- }
- if ($this->options['json']) {
- return json_encode($data, JSON_PRETTY_PRINT) ?: '';
- }
- return $data;
- }
- /**
- * @param PageInterface $page
- * @return string
- */
- public static function getCleanContent($page)
- {
- $grav = Grav::instance();
- $activePage = $grav['page'];
- // Set active page in grav to the one we are currently processing.
- unset($grav['page']);
- $grav['page'] = $page;
- /** @var Twig $twig */
- $twig = $grav['twig'];
- $header = $page->header();
- // @phpstan-ignore-next-line
- if (isset($header->tntsearch['template'])) {
- $processed_page = $twig->processTemplate($header->tntsearch['template'] . '.html.twig', ['page' => $page]);
- $content = $processed_page;
- } else {
- $content = $page->content();
- }
- $content = strip_tags($content);
- $content = preg_replace(['/[ \t]+/', '/\s*$^\s*/m'], [' ', "\n"], $content) ?? $content;
- // Restore active page in Grav.
- unset($grav['page']);
- $grav['page'] = $activePage;
- return $content;
- }
- /**
- * @return void
- */
- public function createIndex()
- {
- $this->tnt->setDatabaseHandle(new GravConnector);
- $indexer = $this->tnt->createIndex($this->index);
- // Disable stemmer for users with older configuration.
- if ($this->options['stemmer'] == 'default') {
- $indexer->setLanguage('no');
- } else {
- $indexer->setLanguage($this->options['stemmer']);
- }
- $indexer->run();
- }
- /**
- * @return void
- * @throws IndexNotFoundException
- */
- public function selectIndex()
- {
- $this->tnt->selectIndex($this->index);
- }
- /**
- * @param object $object
- * @return void
- */
- public function deleteIndex($object)
- {
- if (!$object instanceof Page) {
- return;
- }
- $this->tnt->setDatabaseHandle(new GravConnector);
- try {
- $this->tnt->selectIndex($this->index);
- } catch (IndexNotFoundException $e) {
- return;
- }
- $indexer = $this->tnt->getIndex();
- // Delete existing if it exists
- $indexer->delete($object->route());
- }
- /**
- * @param object $object
- * @return void
- */
- public function updateIndex($object)
- {
- if (!$object instanceof Page) {
- return;
- }
- $this->tnt->setDatabaseHandle(new GravConnector);
- try {
- $this->tnt->selectIndex($this->index);
- } catch (IndexNotFoundException $e) {
- return;
- }
- $indexer = $this->tnt->getIndex();
- // Delete existing if it exists
- $indexer->delete($object->route());
- $filter = Grav::instance()['config']->get('plugins.tntsearch.filter');
- if ($filter && array_key_exists('items', $filter)) {
- if (is_string($filter['items'])) {
- $filter['items'] = Yaml::parse($filter['items']);
- }
- $apage = new Page;
- /** @var Collection $collection */
- $collection = $apage->collection($filter, false);
- $path = $object->path();
- if ($path && array_key_exists($path, $collection->toArray())) {
- $fields = $this->indexPageData($object);
- $document = (array) $fields;
- // Insert document
- $indexer->insert($document);
- }
- }
- }
- /**
- * @param PageInterface $page
- * @return object
- */
- public function indexPageData($page)
- {
- $header = (array) $page->header();
- $redirect = (bool) $page->redirect();
- if (!$page->published()) {
- throw new \RuntimeException('not published...');
- }
- if (!$page->routable()) {
- throw new \RuntimeException('not routable...');
- }
- if ($redirect || (isset($header['tntsearch']['index']) && $header['tntsearch']['index'] === false )) {
- throw new \RuntimeException('redirect only...');
- }
- $route = $page->route();
- $fields = new \stdClass();
- $fields->id = $route;
- $fields->name = $page->title();
- $fields->content = static::getCleanContent($page);
- Grav::instance()->fireEvent('onTNTSearchIndex', new Event(['page' => $page, 'fields' => $fields]));
- return $fields;
- }
- }
|