wiki-grav/plugins/tntsearch/classes/GravTNTSearch.php

351 lines
9.9 KiB
PHP

<?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;
}
}