FEATURE: added advanced search form to allow searching by module and version

This commit is contained in:
Will Rossiter 2011-08-04 10:04:53 +12:00
parent 0ba6d8d338
commit 85e5b1b72d
5 changed files with 247 additions and 52 deletions

View File

@ -59,6 +59,21 @@ class DocumentationSearch {
*/
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
*
@ -173,9 +188,37 @@ class DocumentationSearch {
try {
$index = Zend_Search_Lucene::open(self::get_index_location());
Zend_Search_Lucene::setResultSetLimit(200);
$this->results = $index->find($this->getQuery());
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, 'Module'));
}
$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) {
@ -188,10 +231,12 @@ class DocumentationSearch {
*/
public function getSearchResults($request) {
$pageLength = (isset($_GET['length'])) ? (int) $_GET['length'] : 10;
$data = array(
'Results' => null,
'Query' => null,
'Versions' => DBField::create('Text', implode(',', $this->versions)),
'Modules' => DBField::create('Text', implode(',', $this->modules)),
'Title' => _t('DocumentationSearch.SEARCHRESULTS', 'Search Results'),
'TotalResults' => null,
'TotalPages' => null,
@ -216,27 +261,29 @@ class DocumentationSearch {
$results = new DataObjectSet();
foreach($this->results as $k => $hit) {
if($k < ($currentPage-1)*$pageLength || $k >= ($currentPage*$pageLength)) continue;
if($this->results) {
foreach($this->results as $k => $hit) {
if($k < ($currentPage-1)*$pageLength || $k >= ($currentPage*$pageLength)) continue;
$doc = $hit->getDocument();
$doc = $hit->getDocument();
$content = $hit->content;
// do a simple markdown parse of the file
$obj = new ArrayData(array(
'Title' => DBField::create('Varchar', $doc->getFieldValue('Title')),
'BreadcrumbTitle' => DBField::create('HTMLText', $doc->getFieldValue('BreadcrumbTitle')),
'Link' => DBField::create('Varchar',$doc->getFieldValue('Link')),
'Language' => DBField::create('Varchar',$doc->getFieldValue('Language')),
'Version' => DBField::create('Varchar',$doc->getFieldValue('Version')),
'Content' => DBField::create('HTMLText', $content),
'Score' => $hit->score,
'Number' => $k + 1,
'ID' => md5($doc->getFieldValue('Link'))
));
$content = $hit->content;
$obj = new ArrayData(array(
'Title' => DBField::create('Varchar', $doc->getFieldValue('Title')),
'BreadcrumbTitle' => DBField::create('HTMLText', $doc->getFieldValue('BreadcrumbTitle')),
'Link' => DBField::create('Varchar',$doc->getFieldValue('Link')),
'Language' => DBField::create('Varchar',$doc->getFieldValue('Language')),
'Version' => DBField::create('Varchar',$doc->getFieldValue('Version')),
'Module' => DBField::create('Varchar', $doc->getFieldValue('Module')),
'Content' => DBField::create('HTMLText', $content),
'Score' => $hit->score,
'Number' => $k + 1,
'ID' => md5($doc->getFieldValue('Link'))
));
$results->push($obj);
$results->push($obj);
}
}
$data['Results'] = $results;
@ -358,7 +405,7 @@ class DocumentationSearch {
* the search results template or the Atom feed
*/
public function renderResults() {
if(!$this->results) $this->performSearch();
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();

View File

@ -144,7 +144,7 @@ class DocumentationService {
if($version || $lang) {
foreach($entities as $entity) {
if(self::is_registered_entity($entity->getFolder(), $version, $lang)) {
$output[] = $entity;
$output[$entity->getFolder()] = $entity;
}
}
}

View File

@ -156,7 +156,7 @@ class DocumentationViewer extends Controller {
}
// check to see if the module is a valid module. If it isn't, then we
// check to see if the request is a valid entity. If it isn't, then we
// need to throw a 404.
if(!DocumentationService::is_registered_entity($firstParam)) {
return $this->throw404();
@ -246,7 +246,7 @@ class DocumentationViewer extends Controller {
/**
* Returns the current version. If no version is set then it is the current
* set version so need to pull that from the module.
* set version so need to pull that from the {@link Entity}.
*
* @return String
*/
@ -272,10 +272,9 @@ class DocumentationViewer extends Controller {
}
/**
* Return all the available languages for the module.
* Return all the available languages for the {@link Entity}.
*
* @param String - The name of the module
* @return DataObjectSet
* @return array
*/
function getLanguages() {
$entity = $this->getEntity();
@ -291,7 +290,7 @@ class DocumentationViewer extends Controller {
* Get all the versions loaded for the current {@link DocumentationEntity}.
* the filesystem then they are loaded under the 'Current' namespace.
*
* @param String $entity name of module to limit it to eg sapphire
* @param String $entity name of {@link Entity} to limit it to eg sapphire
* @return DataObjectSet
*/
function getVersions($entity = false) {
@ -302,14 +301,12 @@ class DocumentationViewer extends Controller {
$versions = $entity->getVersions();
$output = new DataObjectSet();
if($versions) {
$lang = $this->getLang();
$currentVersion = $this->getVersion();
foreach($versions as $key => $version) {
// work out the link to this version of the documentation.
// @todo Keep the user on their given page rather than redirecting to module.
$linkingMode = ($currentVersion == $version) ? 'current' : 'link';
if(!$version) $version = 'Current';
@ -480,7 +477,7 @@ class DocumentationViewer extends Controller {
}
/**
* Get the module pages under a given page. Recursive call for {@link getEntityPages()}
* Get all the pages under a given page. Recursive call for {@link getEntityPages()}
*
* @todo Need to rethink how to support pages which are pulling content from their children
* i.e if a folder doesn't have 2 then it will load the first file in the folder
@ -607,7 +604,7 @@ class DocumentationViewer extends Controller {
foreach($pages as $i => $title) {
if($title) {
// Don't add module name, already present in Link()
// Don't add entity name, already present in Link()
if($i > 0) $path[] = $title;
$output->push(new ArrayData(array(
@ -651,9 +648,6 @@ class DocumentationViewer extends Controller {
* @return String
*/
public function Link($path = false, $entity = false, $version = false, $lang = false) {
$base = Director::absoluteBaseURL();
// only include the version. Version is optional after all
$version = ($version === null) ? $this->getVersion() : $version;
$lang = (!$lang) ? $this->getLang() : $lang;
@ -668,7 +662,14 @@ class DocumentationViewer extends Controller {
$action = implode('/', $path);
}
$link = Controller::join_links($base, self::get_link_base(), $entity, $lang, $version, $action);
$link = Controller::join_links(
Director::absoluteBaseURL(),
self::get_link_base(),
$entity,
($entity) ? $lang : "", // only include lang for entity - sapphire/en vs en/
($entity) ? $version :"",
$action
);
return $link;
}
@ -733,18 +734,19 @@ class DocumentationViewer extends Controller {
}
/**
* Documentation Basic Search Form
* Documentation Search Form. Allows filtering of the results by many entities
* and multiple versions.
*
* Integrates with sphinx
* @return Form
*/
function DocumentationSearchForm() {
if(!DocumentationSearch::enabled()) return false;
$query = (isset($_REQUEST['Search'])) ? Convert::raw2xml($_REQUEST['Search']) : "";
$q = ($q = $this->getSearchQuery()) ? $q->NoHTML() : "";
$fields = new FieldSet(
new TextField('Search', _t('DocumentationViewer.SEARCH', 'Search'), $query)
new TextField('Search', _t('DocumentationViewer.SEARCH', 'Search'), $q),
new HiddenField('Entities', '', implode(',', array_keys($this->getSearchedEntities()))),
new HiddenField('Versions', '', implode(',', $this->getSearchedVersions()))
);
$actions = new FieldSet(
@ -759,22 +761,137 @@ class DocumentationViewer extends Controller {
return $form;
}
/**
* Return an array of folders and titles
*
* @return array
*/
function getSearchedEntities() {
$entities = array();
if(isset($_REQUEST['Entities'])) {
if(is_array($_REQUEST['Entities'])) {
$entities = Convert::raw2att($_REQUEST['Entities']);
}
else {
$entities = explode(',', Convert::raw2att($_REQUEST['Entities']));
$entities = array_combine($entities, $entities);
}
}
else if($entity = $this->getEntity()) {
$entities[$entity->getFolder()] = Convert::raw2att($entity->getTitle());
}
return $entities;
}
/**
* Return an array of versions that we're allowed to return
*
* @return array
*/
function getSearchedVersions() {
$versions = array();
if(isset($_REQUEST['Versions'])) {
if(is_array($_REQUEST['Versions'])) {
$versions = Convert::raw2att($_REQUEST['Versions']);
$versions = array_combine($versions, $versions);
}
else {
$version = Convert::raw2att($_REQUEST['Versions']);
$versions[$version] = $version;
}
}
else if($version = $this->getVersion()) {
$version = Convert::raw2att($version);
$versions[$version] = $version;
}
return $versions;
}
/**
* Return the current search query
*
* @return HTMLText|null
*/
function getSearchQuery() {
if(isset($_REQUEST['Search'])) {
return DBField::create('HTMLText', $_REQUEST['Search']);
}
}
/**
* Past straight to results, display and encode the query
*/
function results($data, $form = false) {
$query = (isset($_REQUEST['Search'])) ? $_REQUEST['Search'] : false;
if(!$query) return $this->httpError('404');
$search = new DocumentationSearch();
$search->setQuery($query);
$search->setVersions($this->getSearchedVersions());
$search->setModules($this->getSearchedEntities());
$search->setOutputController($this);
return $search->renderResults();
}
/**
* Returns an search form which allows people to express more complex rules
* and options than the plain search form.
*
* @todo client side filtering of checkable option based on the module selected.
*
* @return Form
*/
function AdvancedSearchForm() {
$entities = DocumentationService::get_registered_entities();
$versions = array();
foreach($entities as $entity) {
$versions[$entity->getFolder()] = $entity->getVersions();
}
// get a list of all the unique versions
$uniqueVersions = array_unique(self::array_flatten(array_values($versions)));
asort($uniqueVersions);
$uniqueVersions = array_combine($uniqueVersions,$uniqueVersions);
$q = ($q = $this->getSearchQuery()) ? $q->NoHTML() : "";
// klude to take an array of objects down to a simple map
$entities = new DataObjectSet($entities);
$entities = $entities->map('Folder', 'Title');
// if we haven't gone any search limit then we're searching everything
$searchedEntities = $this->getSearchedEntities();
if(count($searchedEntities) < 1) $searchedEntities = $entities;
$searchedVersions = $this->getSearchedVersions();
if(count($searchedVersions) < 1) $searchedVersions = $uniqueVersions;
$fields = new FieldSet(
new TextField('Search', _t('DocumentationViewer.KEYWORDS', 'Keywords'), $q),
new CheckboxSetField('Entities', _t('DocumentationViewer.MODULES', 'Modules'), $entities, $searchedEntities),
new CheckboxSetField('Versions', _t('DocumentationViewer.VERSIONS', 'Versions'),
$uniqueVersions, $searchedVersions
)
);
$actions = new FieldSet(
new FormAction('results', _t('DocumentationViewer.SEARCH', 'Search'))
);
$required = new RequiredFields(array('Search'));
$form = new Form($this, 'AdvancedSearchForm', $fields, $actions, $required);
$form->disableSecurityToken();
$form->setFormMethod('get');
$form->setFormAction(self::$link_base . 'DocumentationSearchForm');
return $form;
}
/**
* Check to see if the currently accessed version is out of date or
* perhaps a future version rather than the stable edition
@ -810,4 +927,26 @@ class DocumentationViewer extends Controller {
return false;
}
/**
* Flattens an array
*
* @param array
* @return array
*/
public static function array_flatten($array) {
if(!is_array($array)) return false;
$output = array();
foreach($array as $k => $v) {
if(is_array($v)) {
$output = array_merge($output, self::array_flatten($v));
}
else {
$output[$k] = $v;
}
}
return $output;
}
}

View File

@ -73,15 +73,18 @@ class RebuildLuceneDocsIndex extends BuildTask {
$content = $page->getMarkdown();
if($content) $content = Markdown($content);
$entity = ($entity = $page->getEntity()) ? $entity->getTitle() : "";
$doc->addField(Zend_Search_Lucene_Field::Text('content', $content));
$doc->addField($titleField = Zend_Search_Lucene_Field::Text('Title', $page->getTitle()));
$doc->addField($breadcrumbField = Zend_Search_Lucene_Field::Text('BreadcrumbTitle', $page->getBreadcrumbTitle()));
$doc->addField(Zend_Search_Lucene_Field::Keyword('Version', $page->getVersion()));
$doc->addField(Zend_Search_Lucene_Field::Keyword('Language', $page->getLang()));
$doc->addField(Zend_Search_Lucene_Field::Keyword('Entity', $entity));
$doc->addField(Zend_Search_Lucene_Field::Keyword('Link', $page->Link()));
// custom boosts
$titleField->boost = 1.5;
$titleField->boost = 3;
$breadcrumbField->boost = 1.5;
foreach(DocumentationSearch::$boost_by_path as $pathExpr => $boost) {
if(preg_match($pathExpr, $page->getRelativePath())) $doc->boost = $boost;

View File

@ -1,7 +1,10 @@
<div id="documentation-page">
<div id="content-column">
<p>Your search for <strong>&quot;$Query.XML&quot;</strong> found $TotalResults result<% if TotalResults != 1 %>s<% end_if %>.</p>
<% if Modules || Versions %>
<p>Limited search to <% if Modules %>$Modules <% if Versions %>of<% end_if %><% end_if %> <% if Versions %>versions $Versions<% end_if %>
<% end_if %>
<% if Results %>
<p>Showing page $ThisPage of $TotalPages</p>
@ -41,6 +44,9 @@
</div>
<div id="sidebar-column">
<div class="sidebar-box">
<h4><% _t('ADVANCEDSEARCH', 'Advanced Search') %></h4>
$AdvancedSearchForm
</div>
</div>
</div>