silverstripe-docsviewer/code/DocumentationSearch.php

444 lines
13 KiB
PHP
Executable File

<?php
set_include_path(
dirname(dirname(__FILE__)) . '/thirdparty/'. PATH_SEPARATOR .
get_include_path()
);
require_once 'Zend/Search/Lucene.php';
/**
* Documentation Search powered by Lucene. You will need Zend_Lucene installed
* on your path.
*
* To rebuild the indexes run the {@link RebuildLuceneDocsIndex} task. You may
* wish to setup a cron job to remake the indexes on a regular basis.
*
* This class has the ability to generate an OpenSearch RSS formatted feeds
* simply by using the URL:
*
* <code>
* yoursite.com/search/?q=Foo&format=rss. // Format can either be specified as rss or left off.
* </code>
*
* To get a specific amount of results you can also use the modifiers start and
* limit:
*
* <code>
* yoursite.com/search/?q=Foo&start=10&limit=10
* </code>
*
* @package docsviewer
*/
class DocumentationSearch
{
/**
* @var bool - Is search enabled
*/
private static $enabled = false;
/**
* @var bool - Is advanced search enabled
*/
private static $advanced_search_enabled = true;
/**
* @var string - OpenSearch metadata. Please use {@link DocumentationSearch::set_meta_data()}
*/
private static $meta_data = array();
/**
* @var Array Regular expression mapped to a "boost factor" for the searched document.
* Defaults to 1.0, lower to decrease relevancy. Requires reindex.
* Uses {@link DocumentationPage->getRelativePath()} for comparison.
*/
private static $boost_by_path = array();
/**
* @var ArrayList - Results
*/
private $results;
/**
* @var int
*/
private $totalResults;
/**
* @var string
*/
private $query;
/**
* @var Controller
*/
private $outputController;
/**
* Optionally filter by module and version
*
* @var array
*/
private $modules, $versions;
public function setModules($modules)
{
$this->modules = $modules;
}
public function setVersions($versions)
{
$this->versions = $versions;
}
/**
* Set the current search query
*
* @param string
*/
public function setQuery($query)
{
$this->query = $query;
}
/**
* Returns the current search query
*
* @return string
*/
public function getQuery()
{
return $this->query;
}
/**
* Sets the {@link DocumentationViewer} or {@link DocumentationSearch} instance which this search is rendering
* on based on whether it is the results display or RSS feed
*
* @param Controller
*/
public function setOutputController($controller)
{
$this->outputController = $controller;
}
/**
* Folder name for indexes (in the temp folder).
*
* @config
* @var string
*/
private static $index_location;
/**
* @return string
*/
public static function get_index_location()
{
$location = Config::inst()->get('DocumentationSearch', 'index_location');
if (!$location) {
return Controller::join_links(TEMP_FOLDER, 'RebuildLuceneDocsIndex');
}
return $location;
}
/**
* Perform a search query on the index
*/
public function performSearch()
{
try {
$index = Zend_Search_Lucene::open(self::get_index_location());
Zend_Search_Lucene::setResultSetLimit(100);
$query = new Zend_Search_Lucene_Search_Query_Boolean();
$term = Zend_Search_Lucene_Search_QueryParser::parse($this->getQuery());
$query->addSubquery($term, true);
if ($this->modules) {
$moduleQuery = new Zend_Search_Lucene_Search_Query_MultiTerm();
foreach ($this->modules as $module) {
$moduleQuery->addTerm(new Zend_Search_Lucene_Index_Term($module, 'Entity'));
}
$query->addSubquery($moduleQuery, true);
}
if ($this->versions) {
$versionQuery = new Zend_Search_Lucene_Search_Query_MultiTerm();
foreach ($this->versions as $version) {
$versionQuery->addTerm(new Zend_Search_Lucene_Index_Term($version, 'Version'));
}
$query->addSubquery($versionQuery, true);
}
$er = error_reporting();
error_reporting('E_ALL ^ E_NOTICE');
$this->results = $index->find($query);
error_reporting($er);
$this->totalResults = $index->numDocs();
} catch (Zend_Search_Lucene_Exception $e) {
user_error($e .'. Ensure you have run the rebuld task (/dev/tasks/RebuildLuceneDocsIndex)', E_USER_ERROR);
}
}
/**
* @return ArrayData
*/
public function getSearchResults($request)
{
$pageLength = (isset($_GET['length'])) ? (int) $_GET['length'] : 10;
$data = array(
'Results' => null,
'Query' => null,
'Versions' => DBField::create_field('Text', implode(', ', $this->versions)),
'Modules' => DBField::create_field('Text', implode(', ', $this->modules)),
'Title' => _t('DocumentationSearch.SEARCHRESULTS', 'Search Results'),
'TotalResults' => null,
'TotalPages' => null,
'ThisPage' => null,
'StartResult' => null,
'PageLength' => $pageLength,
'EndResult' => null,
'PrevUrl' => DBField::create_field('Text', 'false'),
'NextUrl' => DBField::create_field('Text', 'false'),
'SearchPages' => new ArrayList()
);
$start = ($request->requestVar('start')) ? (int)$request->requestVar('start') : 0;
$query = ($request->requestVar('q')) ? $request->requestVar('q') : '';
$currentPage = floor($start / $pageLength) + 1;
$totalPages = ceil(count($this->results) / $pageLength);
if ($totalPages == 0) {
$totalPages = 1;
}
if ($currentPage > $totalPages) {
$currentPage = $totalPages;
}
$results = new ArrayList();
if ($this->results) {
foreach ($this->results as $k => $hit) {
if ($k < ($currentPage-1)*$pageLength || $k >= ($currentPage*$pageLength)) {
continue;
}
$doc = $hit->getDocument();
$content = $hit->content;
$obj = new ArrayData(
array(
'Title' => DBField::create_field('Varchar', $doc->getFieldValue('Title')),
'BreadcrumbTitle' => DBField::create_field('HTMLText', $doc->getFieldValue('BreadcrumbTitle')),
'Link' => DBField::create_field('Varchar', $doc->getFieldValue('Link')),
'Language' => DBField::create_field('Varchar', $doc->getFieldValue('Language')),
'Version' => DBField::create_field('Varchar', $doc->getFieldValue('Version')),
'Entity' => DBField::create_field('Varchar', $doc->getFieldValue('Entity')),
'Content' => DBField::create_field('HTMLText', $content),
'Score' => $hit->score,
'Number' => $k + 1,
'ID' => md5($doc->getFieldValue('Link'))
)
);
$results->push($obj);
}
}
$data['Results'] = $results;
$data['Query'] = DBField::create_field('Text', $query);
$data['TotalResults'] = DBField::create_field('Text', count($this->results));
$data['TotalPages'] = DBField::create_field('Text', $totalPages);
$data['ThisPage'] = DBField::create_field('Text', $currentPage);
$data['StartResult'] = $start + 1;
$data['EndResult'] = $start + count($results);
// Pagination links
if ($currentPage > 1) {
$data['PrevUrl'] = DBField::create_field(
'Text',
$this->buildQueryUrl(array('start' => ($currentPage - 2) * $pageLength))
);
}
if ($currentPage < $totalPages) {
$data['NextUrl'] = DBField::create_field(
'Text',
$this->buildQueryUrl(array('start' => $currentPage * $pageLength))
);
}
if ($totalPages > 1) {
// Always show a certain number of pages at the start
for ($i = 1; $i <= $totalPages; $i++) {
$obj = new DataObject();
$obj->IsEllipsis = false;
$obj->PageNumber = $i;
$obj->Link = $this->buildQueryUrl(
array(
'start' => ($i - 1) * $pageLength
)
);
$obj->Current = false;
if ($i == $currentPage) {
$obj->Current = true;
}
$data['SearchPages']->push($obj);
}
}
return new ArrayData($data);
}
/**
* Build a nice query string for the results
*
* @return string
*/
private function buildQueryUrl($params)
{
$url = parse_url($_SERVER['REQUEST_URI']);
if (! array_key_exists('query', $url)) {
$url['query'] = '';
}
parse_str($url['query'], $url['query']);
if (! is_array($url['query'])) {
$url['query'] = array();
}
// Remove 'start parameter if it exists
if (array_key_exists('start', $url['query'])) {
unset($url['query']['start']);
}
// Add extra parameters from argument
$url['query'] = array_merge($url['query'], $params);
$url['query'] = http_build_query($url['query']);
$url = $url['path'] . ($url['query'] ? '?'.$url['query'] : '');
return $url;
}
/**
* @return int
*/
public function getTotalResults()
{
return (int) $this->totalResults;
}
/**
* Optimizes the search indexes on the File System
*
* @return void
*/
public function optimizeIndex()
{
$index = Zend_Search_Lucene::open(self::get_index_location());
if ($index) {
$index->optimize();
}
}
/**
* @return String
*/
public function getTitle()
{
return ($this->outputController) ? $this->outputController->Title : _t('DocumentationSearch.SEARCH', 'Search');
}
/**
* OpenSearch MetaData fields. For a list of fields consult
* {@link self::get_meta_data()}
*
* @param array
*/
public static function set_meta_data($data)
{
if (is_array($data)) {
foreach ($data as $key => $value) {
self::$meta_data[strtolower($key)] = $value;
}
} else {
user_error("set_meta_data must be passed an array", E_USER_ERROR);
}
}
/**
* Returns the meta data needed by opensearch.
*
* @return array
*/
public static function get_meta_data()
{
$data = self::$meta_data;
$defaults = array(
'Description' => _t('DocumentationViewer.OPENSEARCHDESC', 'Search the documentation'),
'Tags' => _t('DocumentationViewer.OPENSEARCHTAGS', 'documentation'),
'Contact' => Config::inst()->get('Email', 'admin_email'),
'ShortName' => _t('DocumentationViewer.OPENSEARCHNAME', 'Documentation Search'),
'Author' => 'SilverStripe'
);
foreach ($defaults as $key => $value) {
if (isset($data[$key])) {
$defaults[$key] = $data[$key];
}
}
return $defaults;
}
/**
* Renders the search results into a template. Either the search results
* template or the Atom feed.
*/
public function renderResults()
{
if (!$this->results && $this->query) {
$this->performSearch();
}
if (!$this->outputController) {
return user_error('Call renderResults() on a DocumentationViewer instance.', E_USER_ERROR);
}
$request = $this->outputController->getRequest();
$data = $this->getSearchResults($request);
$templates = array('DocumentationViewer_search');
if ($request->requestVar('format') && $request->requestVar('format') == "atom") {
// alter the fields for the opensearch xml.
$title = ($title = $this->getTitle()) ? ' - '. $title : "";
$link = Controller::join_links(
$this->outputController->Link(),
'DocumentationOpenSearchController/description/'
);
$data->setField('Title', $data->Title . $title);
$data->setField('DescriptionURL', $link);
array_unshift($templates, 'OpenSearchResults');
}
return $this->outputController->customise($data)->renderWith($templates);
}
}