351 lines
9.9 KiB
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;
|
|
}
|
|
}
|